thumbgate 1.18.0 → 1.20.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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +32 -10
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +39 -2
- package/config/model-candidates.json +31 -0
- package/package.json +33 -8
- package/public/compare.html +12 -0
- package/public/federal.html +375 -0
- package/public/guide.html +2 -2
- package/public/index.html +61 -5
- package/public/learn.html +43 -0
- package/public/numbers.html +2 -2
- package/public/pro.html +3 -22
- package/scripts/activation-tracker.js +127 -0
- package/scripts/auto-promote-gates.js +153 -7
- package/scripts/auto-wire-hooks.js +50 -29
- package/scripts/cli-feedback.js +9 -1
- package/scripts/feedback-loop.js +14 -1
- package/scripts/memory-scope-readiness.js +315 -0
- package/scripts/plausible-server-events.js +162 -0
- package/scripts/rate-limiter.js +11 -0
- package/scripts/seo-gsd.js +75 -2
- package/scripts/statusline-links.js +2 -0
- package/scripts/telemetry-analytics.js +1 -0
- package/src/api/server.js +536 -109
package/src/api/server.js
CHANGED
|
@@ -13,11 +13,17 @@ const {
|
|
|
13
13
|
const POSTHOG_API_PATHS = new Set(['/capture', '/batch', '/decide', '/e', '/engage']);
|
|
14
14
|
const POSTHOG_INGEST_HOST = 'us.i.posthog.com';
|
|
15
15
|
const POSTHOG_STATIC_PATH_PREFIX = '/static/';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// Payment Links generated by scripts/stripe-bootstrap-saas-catalog.js
|
|
17
|
+
// (2026-05-14 dispatch run 25883541719). These are tied to the
|
|
18
|
+
// ThumbGate-branded persistent products (metadata.thumbgate_tier=*) in the
|
|
19
|
+
// Stripe catalog, with the per-tier thumbnails wired in. Re-run the
|
|
20
|
+
// bootstrap workflow to regenerate; the new URLs surface in the workflow
|
|
21
|
+
// summary log.
|
|
22
|
+
const FIRST_FAILURE_RULE_CHECKOUT_URL = 'https://buy.stripe.com/fZu28rfCY6zcbO99uj3sI2G';
|
|
23
|
+
const QUICK_READ_CHECKOUT_URL = 'https://buy.stripe.com/5kQ7sL76s1eSaK55e33sI2H';
|
|
24
|
+
const WORKFLOW_TEARDOWN_CHECKOUT_URL = 'https://buy.stripe.com/8x214n2Qc4r44lHayn3sI2I';
|
|
25
|
+
const SPRINT_DIAGNOSTIC_CHECKOUT_URL = 'https://buy.stripe.com/28E00j3Uge1E2dzgWL3sI2J';
|
|
26
|
+
const WORKFLOW_SPRINT_CHECKOUT_URL = 'https://buy.stripe.com/6oU00j8aw2iWdWh9uj3sI2K';
|
|
21
27
|
|
|
22
28
|
function getPosthogProxyPath(pathname) {
|
|
23
29
|
return pathname.slice('/ingest'.length) || '/';
|
|
@@ -87,6 +93,9 @@ const {
|
|
|
87
93
|
const {
|
|
88
94
|
classifyRequester,
|
|
89
95
|
} = require('../../scripts/bot-detection');
|
|
96
|
+
const {
|
|
97
|
+
recordCheckoutFunnelEvent,
|
|
98
|
+
} = require('../../scripts/plausible-server-events');
|
|
90
99
|
const {
|
|
91
100
|
buildCloudflareSandboxPlan,
|
|
92
101
|
} = require('../../scripts/cloudflare-dynamic-sandbox');
|
|
@@ -209,6 +218,7 @@ const CODEX_PLUGIN_PAGE_PATH = path.resolve(__dirname, '../../public/codex-plugi
|
|
|
209
218
|
const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
|
|
210
219
|
const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
|
|
211
220
|
const NUMBERS_PAGE_PATH = path.resolve(__dirname, '../../public/numbers.html');
|
|
221
|
+
const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
|
|
212
222
|
const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
|
|
213
223
|
const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
|
|
214
224
|
const COMPARE_DIR = path.resolve(__dirname, '../../public/compare');
|
|
@@ -1489,50 +1499,11 @@ function buildCheckoutIntentHref(baseUrl, metadata = {}, overrides = {}) {
|
|
|
1489
1499
|
|
|
1490
1500
|
function renderCheckoutIntentPage({
|
|
1491
1501
|
confirmHref,
|
|
1492
|
-
firstRuleCheckoutHref,
|
|
1493
|
-
quickReadCheckoutHref,
|
|
1494
|
-
workflowTeardownCheckoutHref,
|
|
1495
1502
|
workflowIntakeHref,
|
|
1496
|
-
teamOptionsHref,
|
|
1497
|
-
diagnosticCheckoutHref,
|
|
1498
|
-
sprintCheckoutHref,
|
|
1499
|
-
sprintDiagnosticPriceDollars = 499,
|
|
1500
|
-
workflowSprintPriceDollars = 1500,
|
|
1501
1503
|
}) {
|
|
1502
1504
|
const safeConfirmHref = escapeHtmlAttribute(confirmHref);
|
|
1503
|
-
const safeFirstRuleCheckoutHref = firstRuleCheckoutHref
|
|
1504
|
-
? escapeHtmlAttribute(firstRuleCheckoutHref)
|
|
1505
|
-
: '';
|
|
1506
|
-
const safeQuickReadCheckoutHref = quickReadCheckoutHref
|
|
1507
|
-
? escapeHtmlAttribute(quickReadCheckoutHref)
|
|
1508
|
-
: '';
|
|
1509
|
-
const safeWorkflowTeardownCheckoutHref = workflowTeardownCheckoutHref
|
|
1510
|
-
? escapeHtmlAttribute(workflowTeardownCheckoutHref)
|
|
1511
|
-
: '';
|
|
1512
1505
|
const safeWorkflowIntakeHref = escapeHtmlAttribute(workflowIntakeHref);
|
|
1513
|
-
|
|
1514
|
-
const safeDiagnosticCheckoutHref = diagnosticCheckoutHref
|
|
1515
|
-
? escapeHtmlAttribute(diagnosticCheckoutHref)
|
|
1516
|
-
: '';
|
|
1517
|
-
const safeSprintCheckoutHref = sprintCheckoutHref
|
|
1518
|
-
? escapeHtmlAttribute(sprintCheckoutHref)
|
|
1519
|
-
: '';
|
|
1520
|
-
const diagnosticAction = safeDiagnosticCheckoutHref
|
|
1521
|
-
? `<a data-i="sprint_diagnostic_checkout" href="${safeDiagnosticCheckoutHref}">Book $${sprintDiagnosticPriceDollars} diagnostic</a>`
|
|
1522
|
-
: '';
|
|
1523
|
-
const sprintAction = safeSprintCheckoutHref
|
|
1524
|
-
? `<a data-i="workflow_sprint_checkout" href="${safeSprintCheckoutHref}">Start $${workflowSprintPriceDollars} sprint</a>`
|
|
1525
|
-
: '';
|
|
1526
|
-
const firstRuleAction = safeFirstRuleCheckoutHref
|
|
1527
|
-
? `<a data-i="first_failure_rule_checkout" href="${safeFirstRuleCheckoutHref}">Pay $1 first rule</a>`
|
|
1528
|
-
: '';
|
|
1529
|
-
const quickReadAction = safeQuickReadCheckoutHref
|
|
1530
|
-
? `<a data-i="quick_read_checkout" href="${safeQuickReadCheckoutHref}">Pay $19 quick read</a>`
|
|
1531
|
-
: '';
|
|
1532
|
-
const teardownAction = safeWorkflowTeardownCheckoutHref
|
|
1533
|
-
? `<a data-i="workflow_teardown_checkout" href="${safeWorkflowTeardownCheckoutHref}">Pay $99 teardown</a>`
|
|
1534
|
-
: '';
|
|
1535
|
-
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>`;
|
|
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>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><a class="secondary" data-i="workflow_sprint_intake" href="${safeWorkflowIntakeHref}">Not sure yet? Send the workflow first</a><p class="choice-note">One checkout path here. Diagnostics, sprints, kits, and custom services live outside the Pro checkout so the buyer is not asked to choose between unrelated offers.</p><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><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>`;
|
|
1536
1507
|
}
|
|
1537
1508
|
|
|
1538
1509
|
function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneyState(req, parsed)) {
|
|
@@ -2488,10 +2459,19 @@ function renderRobotsTxt(runtimeConfig) {
|
|
|
2488
2459
|
return [
|
|
2489
2460
|
'User-agent: *',
|
|
2490
2461
|
'Allow: /',
|
|
2462
|
+
// 2026-05-12: every crawler GET on /checkout/pro creates a live Stripe
|
|
2463
|
+
// session even when no human is on the other end. Stripe sees 50 sessions
|
|
2464
|
+
// in 24h, 0 paid, 0 email captured. Disallow so non-human fetchers stop
|
|
2465
|
+
// inflating the "checkout starts" metric and creating zombie sessions.
|
|
2466
|
+
// Real humans still reach checkout via JS-driven clicks (not crawled).
|
|
2467
|
+
'Disallow: /checkout/',
|
|
2468
|
+
'Disallow: /v1/billing/',
|
|
2491
2469
|
'',
|
|
2492
2470
|
'# AI crawler access — allow all major LLM crawlers',
|
|
2493
2471
|
'User-agent: GPTBot',
|
|
2494
2472
|
'Allow: /',
|
|
2473
|
+
'Disallow: /checkout/',
|
|
2474
|
+
'Disallow: /v1/billing/',
|
|
2495
2475
|
'',
|
|
2496
2476
|
'User-agent: ClaudeBot',
|
|
2497
2477
|
'Allow: /',
|
|
@@ -4373,6 +4353,28 @@ async function addContext(){
|
|
|
4373
4353
|
return;
|
|
4374
4354
|
}
|
|
4375
4355
|
|
|
4356
|
+
if (isGetLikeRequest && (pathname === '/federal' || pathname === '/federal.html' || pathname === '/government' || pathname === '/gov')) {
|
|
4357
|
+
// Federal lead-gen page. Routed through servePublicMarketingPage so agency
|
|
4358
|
+
// arrivals via SBIR / GSA / outbound channels capture UTM attribution and
|
|
4359
|
+
// landing_page_view telemetry for downstream pilot-pipeline analysis.
|
|
4360
|
+
// /government and /gov redirect-friendly aliases serve the same page so
|
|
4361
|
+
// common search queries land correctly.
|
|
4362
|
+
try {
|
|
4363
|
+
servePublicMarketingPage({
|
|
4364
|
+
req,
|
|
4365
|
+
res,
|
|
4366
|
+
parsed,
|
|
4367
|
+
hostedConfig,
|
|
4368
|
+
isHeadRequest,
|
|
4369
|
+
renderHtml: () => fs.readFileSync(FEDERAL_PAGE_PATH, 'utf-8'),
|
|
4370
|
+
extraTelemetry: { pageType: 'federal' },
|
|
4371
|
+
});
|
|
4372
|
+
} catch {
|
|
4373
|
+
sendJson(res, 404, { error: 'Federal page not found' });
|
|
4374
|
+
}
|
|
4375
|
+
return;
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4376
4378
|
if (isGetLikeRequest && pathname === '/learn/learn.css') {
|
|
4377
4379
|
try {
|
|
4378
4380
|
const cssPath = path.join(LEARN_DIR, 'learn.css');
|
|
@@ -4511,8 +4513,43 @@ async function addContext(){
|
|
|
4511
4513
|
const isConfirmedCheckout = confirmParam === '1'
|
|
4512
4514
|
|| confirmParam === 'true'
|
|
4513
4515
|
|| req.method === 'POST';
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
+
// Plausible funnel event #1 of 3: page view. Fired before interstitial
|
|
4517
|
+
// deflection so we get the full top-of-funnel count, with isBot as a
|
|
4518
|
+
// prop so the dashboard can filter human vs. crawler traffic. Fire-and-forget.
|
|
4519
|
+
recordCheckoutFunnelEvent('view', {
|
|
4520
|
+
page: '/checkout/pro',
|
|
4521
|
+
referrer: req.headers.referer || req.headers.referrer,
|
|
4522
|
+
forwardedFor: req.headers['x-forwarded-for'],
|
|
4523
|
+
remoteAddr: req.socket?.remoteAddress,
|
|
4524
|
+
userAgent: req.headers['user-agent'],
|
|
4525
|
+
props: {
|
|
4526
|
+
traceId,
|
|
4527
|
+
isBot: botClassification.isBot ? 'true' : 'false',
|
|
4528
|
+
botReason: botClassification.isBot ? botClassification.reason : undefined,
|
|
4529
|
+
isConfirmed: isConfirmedCheckout ? 'true' : 'false',
|
|
4530
|
+
utmSource: analyticsMetadata.utmSource,
|
|
4531
|
+
utmMedium: analyticsMetadata.utmMedium,
|
|
4532
|
+
utmCampaign: analyticsMetadata.utmCampaign,
|
|
4533
|
+
planId: analyticsMetadata.planId,
|
|
4534
|
+
},
|
|
4535
|
+
}).catch(() => {});
|
|
4536
|
+
// Render the interstitial for ALL non-confirmed GETs (bot or human),
|
|
4537
|
+
// not just bot traffic. Rationale: a raw GET on /checkout/pro currently
|
|
4538
|
+
// 302s straight to a fresh Stripe `cs_live_*` session, regardless of
|
|
4539
|
+
// whether the visitor is a search crawler, a link-preview fetcher, or
|
|
4540
|
+
// a confused human who doesn't yet know what they're paying for.
|
|
4541
|
+
// That created the 50-zombie-sessions / 0-paid pattern the CEO flagged
|
|
4542
|
+
// 2026-05-13. Every visitor now sees the $19/mo confirmation page first
|
|
4543
|
+
// and must click "Pay $19/mo with Stripe →" (which sets confirm=1) to
|
|
4544
|
+
// trigger the Stripe-session creation + redirect. Counter-risk: one
|
|
4545
|
+
// extra click on the human path. Mitigated because the interstitial
|
|
4546
|
+
// also serves as a value-preview ("6 paying customers, MIT open source,
|
|
4547
|
+
// cancel anytime"), which typically lifts conversion more than the
|
|
4548
|
+
// click-friction costs.
|
|
4549
|
+
if (!isConfirmedCheckout) {
|
|
4550
|
+
const eventType = botClassification.isBot
|
|
4551
|
+
? 'checkout_bot_deflected'
|
|
4552
|
+
: 'checkout_interstitial_view';
|
|
4516
4553
|
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
4517
4554
|
eventType,
|
|
4518
4555
|
clientType: 'web',
|
|
@@ -4531,6 +4568,7 @@ async function addContext(){
|
|
|
4531
4568
|
ctaId: analyticsMetadata.ctaId,
|
|
4532
4569
|
ctaPlacement: analyticsMetadata.ctaPlacement,
|
|
4533
4570
|
planId: analyticsMetadata.planId,
|
|
4571
|
+
isBot: botClassification.isBot ? 'true' : 'false',
|
|
4534
4572
|
reason: botClassification.reason,
|
|
4535
4573
|
}, req.headers, eventType);
|
|
4536
4574
|
const workflowIntakeHref = buildCheckoutIntentHref(`${hostedConfig.appOrigin}/#workflow-sprint-intake`, analyticsMetadata, {
|
|
@@ -4540,67 +4578,9 @@ async function addContext(){
|
|
|
4540
4578
|
ctaPlacement: 'checkout_interstitial',
|
|
4541
4579
|
planId: 'team',
|
|
4542
4580
|
});
|
|
4543
|
-
const teamOptionsHref = buildCheckoutIntentHref(`${hostedConfig.appOrigin}/guides/ai-agent-governance-sprint`, analyticsMetadata, {
|
|
4544
|
-
utmMedium: 'checkout_interstitial_paid_path',
|
|
4545
|
-
utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_team_paid_path',
|
|
4546
|
-
ctaId: 'checkout_interstitial_team_paid_path',
|
|
4547
|
-
ctaPlacement: 'checkout_interstitial',
|
|
4548
|
-
planId: 'team',
|
|
4549
|
-
});
|
|
4550
|
-
const firstRuleCheckoutHref = buildCheckoutIntentHref(FIRST_FAILURE_RULE_CHECKOUT_URL, analyticsMetadata, {
|
|
4551
|
-
utmMedium: 'checkout_interstitial_paid_path',
|
|
4552
|
-
utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_first_failure_rule',
|
|
4553
|
-
ctaId: 'checkout_interstitial_first_failure_rule_checkout',
|
|
4554
|
-
ctaPlacement: 'checkout_interstitial',
|
|
4555
|
-
planId: 'first_failure_rule',
|
|
4556
|
-
});
|
|
4557
|
-
const quickReadCheckoutHref = buildCheckoutIntentHref(QUICK_READ_CHECKOUT_URL, analyticsMetadata, {
|
|
4558
|
-
utmMedium: 'checkout_interstitial_paid_path',
|
|
4559
|
-
utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_quick_read',
|
|
4560
|
-
ctaId: 'checkout_interstitial_quick_read_checkout',
|
|
4561
|
-
ctaPlacement: 'checkout_interstitial',
|
|
4562
|
-
planId: 'quick_read',
|
|
4563
|
-
});
|
|
4564
|
-
const workflowTeardownCheckoutHref = buildCheckoutIntentHref(WORKFLOW_TEARDOWN_CHECKOUT_URL, analyticsMetadata, {
|
|
4565
|
-
utmMedium: 'checkout_interstitial_paid_path',
|
|
4566
|
-
utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_teardown',
|
|
4567
|
-
ctaId: 'checkout_interstitial_workflow_teardown_checkout',
|
|
4568
|
-
ctaPlacement: 'checkout_interstitial',
|
|
4569
|
-
planId: 'workflow_teardown',
|
|
4570
|
-
});
|
|
4571
|
-
const diagnosticCheckoutHref = buildCheckoutIntentHref(
|
|
4572
|
-
hostedConfig.sprintDiagnosticCheckoutUrl || SPRINT_DIAGNOSTIC_CHECKOUT_URL,
|
|
4573
|
-
analyticsMetadata,
|
|
4574
|
-
{
|
|
4575
|
-
utmMedium: 'checkout_interstitial_paid_path',
|
|
4576
|
-
utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_diagnostic',
|
|
4577
|
-
ctaId: 'checkout_interstitial_sprint_diagnostic_checkout',
|
|
4578
|
-
ctaPlacement: 'checkout_interstitial',
|
|
4579
|
-
planId: 'sprint_diagnostic',
|
|
4580
|
-
}
|
|
4581
|
-
);
|
|
4582
|
-
const sprintCheckoutHref = buildCheckoutIntentHref(
|
|
4583
|
-
hostedConfig.workflowSprintCheckoutUrl || WORKFLOW_SPRINT_CHECKOUT_URL,
|
|
4584
|
-
analyticsMetadata,
|
|
4585
|
-
{
|
|
4586
|
-
utmMedium: 'checkout_interstitial_paid_path',
|
|
4587
|
-
utmCampaign: analyticsMetadata.utmCampaign || 'checkout_interstitial_workflow_sprint',
|
|
4588
|
-
ctaId: 'checkout_interstitial_workflow_sprint_checkout',
|
|
4589
|
-
ctaPlacement: 'checkout_interstitial',
|
|
4590
|
-
planId: 'workflow_sprint',
|
|
4591
|
-
}
|
|
4592
|
-
);
|
|
4593
4581
|
const html = renderCheckoutIntentPage({
|
|
4594
4582
|
confirmHref: buildCheckoutConfirmHref(parsed),
|
|
4595
|
-
firstRuleCheckoutHref,
|
|
4596
|
-
quickReadCheckoutHref,
|
|
4597
|
-
workflowTeardownCheckoutHref,
|
|
4598
4583
|
workflowIntakeHref,
|
|
4599
|
-
teamOptionsHref,
|
|
4600
|
-
diagnosticCheckoutHref,
|
|
4601
|
-
sprintCheckoutHref,
|
|
4602
|
-
sprintDiagnosticPriceDollars: hostedConfig.sprintDiagnosticPriceDollars || 499,
|
|
4603
|
-
workflowSprintPriceDollars: hostedConfig.workflowSprintPriceDollars || 1500,
|
|
4604
4584
|
botClassification,
|
|
4605
4585
|
});
|
|
4606
4586
|
sendHtml(res, 200, html, responseHeaders);
|
|
@@ -4651,6 +4631,26 @@ async function addContext(){
|
|
|
4651
4631
|
referrer: analyticsMetadata.referrer,
|
|
4652
4632
|
referrerHost: analyticsMetadata.referrerHost,
|
|
4653
4633
|
}, req.headers, 'checkout_bootstrap');
|
|
4634
|
+
// Plausible funnel event #2 of 3: email submitted (bootstrap fired with
|
|
4635
|
+
// a valid email present — the user has supplied the Stripe-receipt
|
|
4636
|
+
// address). Only fires when normalizedCheckoutEmail was non-empty.
|
|
4637
|
+
if (normalizedCheckoutEmail) {
|
|
4638
|
+
recordCheckoutFunnelEvent('emailSubmitted', {
|
|
4639
|
+
page: '/checkout/pro',
|
|
4640
|
+
referrer: req.headers.referer || req.headers.referrer,
|
|
4641
|
+
forwardedFor: req.headers['x-forwarded-for'],
|
|
4642
|
+
remoteAddr: req.socket?.remoteAddress,
|
|
4643
|
+
userAgent: req.headers['user-agent'],
|
|
4644
|
+
props: {
|
|
4645
|
+
traceId,
|
|
4646
|
+
utmSource: analyticsMetadata.utmSource,
|
|
4647
|
+
utmMedium: analyticsMetadata.utmMedium,
|
|
4648
|
+
utmCampaign: analyticsMetadata.utmCampaign,
|
|
4649
|
+
planId: analyticsMetadata.planId,
|
|
4650
|
+
billingCycle: analyticsMetadata.billingCycle,
|
|
4651
|
+
},
|
|
4652
|
+
}).catch(() => {});
|
|
4653
|
+
}
|
|
4654
4654
|
|
|
4655
4655
|
try {
|
|
4656
4656
|
const result = await createCheckoutSession({
|
|
@@ -4670,6 +4670,56 @@ async function addContext(){
|
|
|
4670
4670
|
});
|
|
4671
4671
|
|
|
4672
4672
|
if (result.url) {
|
|
4673
|
+
// Plausible funnel event #3 of 3: Stripe redirect started. Fires
|
|
4674
|
+
// before the 302 so the event reaches Plausible even if the
|
|
4675
|
+
// browser leaves the origin immediately. Fire-and-forget.
|
|
4676
|
+
recordCheckoutFunnelEvent('stripeRedirect', {
|
|
4677
|
+
page: '/checkout/pro',
|
|
4678
|
+
referrer: req.headers.referer || req.headers.referrer,
|
|
4679
|
+
forwardedFor: req.headers['x-forwarded-for'],
|
|
4680
|
+
remoteAddr: req.socket?.remoteAddress,
|
|
4681
|
+
userAgent: req.headers['user-agent'],
|
|
4682
|
+
props: {
|
|
4683
|
+
traceId,
|
|
4684
|
+
stripeSessionId: result.sessionId,
|
|
4685
|
+
utmSource: analyticsMetadata.utmSource,
|
|
4686
|
+
utmMedium: analyticsMetadata.utmMedium,
|
|
4687
|
+
utmCampaign: analyticsMetadata.utmCampaign,
|
|
4688
|
+
planId: analyticsMetadata.planId,
|
|
4689
|
+
billingCycle: analyticsMetadata.billingCycle,
|
|
4690
|
+
},
|
|
4691
|
+
}).catch(() => {});
|
|
4692
|
+
appendBestEffortTelemetry(FEEDBACK_DIR, {
|
|
4693
|
+
eventType: 'stripe_redirect_started',
|
|
4694
|
+
clientType: 'web',
|
|
4695
|
+
installId: bootstrapBody.installId,
|
|
4696
|
+
acquisitionId: analyticsMetadata.acquisitionId,
|
|
4697
|
+
visitorId: analyticsMetadata.visitorId,
|
|
4698
|
+
sessionId: analyticsMetadata.sessionId,
|
|
4699
|
+
traceId,
|
|
4700
|
+
stripeSessionId: result.sessionId,
|
|
4701
|
+
source: analyticsMetadata.source,
|
|
4702
|
+
utmSource: analyticsMetadata.utmSource,
|
|
4703
|
+
utmMedium: analyticsMetadata.utmMedium,
|
|
4704
|
+
utmCampaign: analyticsMetadata.utmCampaign,
|
|
4705
|
+
utmContent: analyticsMetadata.utmContent,
|
|
4706
|
+
utmTerm: analyticsMetadata.utmTerm,
|
|
4707
|
+
creator: analyticsMetadata.creator,
|
|
4708
|
+
community: analyticsMetadata.community,
|
|
4709
|
+
postId: analyticsMetadata.postId,
|
|
4710
|
+
commentId: analyticsMetadata.commentId,
|
|
4711
|
+
campaignVariant: analyticsMetadata.campaignVariant,
|
|
4712
|
+
offerCode: analyticsMetadata.offerCode,
|
|
4713
|
+
landingPath: analyticsMetadata.landingPath,
|
|
4714
|
+
page: '/checkout/pro',
|
|
4715
|
+
ctaId: analyticsMetadata.ctaId,
|
|
4716
|
+
ctaPlacement: analyticsMetadata.ctaPlacement,
|
|
4717
|
+
planId: analyticsMetadata.planId,
|
|
4718
|
+
billingCycle: analyticsMetadata.billingCycle,
|
|
4719
|
+
seatCount: analyticsMetadata.seatCount,
|
|
4720
|
+
referrer: analyticsMetadata.referrer,
|
|
4721
|
+
referrerHost: analyticsMetadata.referrerHost,
|
|
4722
|
+
}, req.headers, 'stripe_redirect_started');
|
|
4673
4723
|
res.writeHead(302, {
|
|
4674
4724
|
...responseHeaders,
|
|
4675
4725
|
Location: result.url,
|
|
@@ -4905,11 +4955,49 @@ async function addContext(){
|
|
|
4905
4955
|
}
|
|
4906
4956
|
|
|
4907
4957
|
if (isGetLikeRequest && pathname === '/health') {
|
|
4908
|
-
|
|
4909
|
-
|
|
4958
|
+
// History (2026-05-12): /health used to return status: 'ok' unconditionally
|
|
4959
|
+
// with zero downstream checks. Uptime monitors saw "healthy" when Stripe
|
|
4960
|
+
// was down, when feedback-dir was unwritable, when env was misconfigured.
|
|
4961
|
+
// The fix is shallow but meaningful: probe each critical subsystem and
|
|
4962
|
+
// surface failures with HTTP 503 + a per-check breakdown.
|
|
4963
|
+
const checks = {};
|
|
4964
|
+
let allOk = true;
|
|
4965
|
+
|
|
4966
|
+
// Check 1: feedback dir exists and is writable.
|
|
4967
|
+
try {
|
|
4968
|
+
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
4969
|
+
fs.accessSync(FEEDBACK_DIR, fs.constants.W_OK);
|
|
4970
|
+
checks.feedbackDir = { ok: true };
|
|
4971
|
+
} catch (err) {
|
|
4972
|
+
checks.feedbackDir = { ok: false, error: err?.code || 'inaccessible' };
|
|
4973
|
+
allOk = false;
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4976
|
+
// Check 2: hosted config resolves the canonical app origin.
|
|
4977
|
+
// If appOrigin is missing/empty, redirects + checkout flow break silently.
|
|
4978
|
+
if (hostedConfig?.appOrigin) {
|
|
4979
|
+
checks.hostedConfig = { ok: true };
|
|
4980
|
+
} else {
|
|
4981
|
+
checks.hostedConfig = { ok: false, error: 'missing_appOrigin' };
|
|
4982
|
+
allOk = false;
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
// Check 3: build metadata loaded. If BUILD_METADATA.buildSha is empty,
|
|
4986
|
+
// Railway didn't inject the deploy SHA — observability is degraded.
|
|
4987
|
+
if (BUILD_METADATA?.buildSha) {
|
|
4988
|
+
checks.buildMetadata = { ok: true };
|
|
4989
|
+
} else {
|
|
4990
|
+
checks.buildMetadata = { ok: false, error: 'missing_buildSha' };
|
|
4991
|
+
allOk = false;
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4994
|
+
const statusCode = allOk ? 200 : 503;
|
|
4995
|
+
sendJson(res, statusCode, {
|
|
4996
|
+
status: allOk ? 'ok' : 'degraded',
|
|
4910
4997
|
version: pkg.version,
|
|
4911
4998
|
buildSha: BUILD_METADATA.buildSha,
|
|
4912
4999
|
uptime: process.uptime(),
|
|
5000
|
+
checks,
|
|
4913
5001
|
deployment: {
|
|
4914
5002
|
appOrigin: hostedConfig.appOrigin,
|
|
4915
5003
|
billingApiBaseUrl: hostedConfig.billingApiBaseUrl,
|
|
@@ -4921,11 +5009,26 @@ async function addContext(){
|
|
|
4921
5009
|
}
|
|
4922
5010
|
|
|
4923
5011
|
if (isGetLikeRequest && pathname === '/healthz') {
|
|
5012
|
+
// /healthz is the deeper internal probe — verifies feedback log + memory log
|
|
5013
|
+
// paths exist and are writable. Returns 503 when either check fails.
|
|
4924
5014
|
const { FEEDBACK_LOG_PATH, MEMORY_LOG_PATH } = requestFeedbackPaths;
|
|
4925
|
-
|
|
4926
|
-
|
|
5015
|
+
const checks = {};
|
|
5016
|
+
let allOk = true;
|
|
5017
|
+
for (const [label, p] of [['feedbackLog', FEEDBACK_LOG_PATH], ['memoryLog', MEMORY_LOG_PATH]]) {
|
|
5018
|
+
try {
|
|
5019
|
+
const dir = path.dirname(p);
|
|
5020
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
5021
|
+
checks[label] = { ok: true };
|
|
5022
|
+
} catch (err) {
|
|
5023
|
+
checks[label] = { ok: false, error: err?.code || 'inaccessible' };
|
|
5024
|
+
allOk = false;
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
sendJson(res, allOk ? 200 : 503, {
|
|
5028
|
+
status: allOk ? 'ok' : 'degraded',
|
|
4927
5029
|
feedbackLogPath: FEEDBACK_LOG_PATH,
|
|
4928
5030
|
memoryLogPath: MEMORY_LOG_PATH,
|
|
5031
|
+
checks,
|
|
4929
5032
|
}, {}, {
|
|
4930
5033
|
headOnly: isHeadRequest,
|
|
4931
5034
|
});
|
|
@@ -5048,6 +5151,114 @@ async function addContext(){
|
|
|
5048
5151
|
return;
|
|
5049
5152
|
}
|
|
5050
5153
|
|
|
5154
|
+
// GET /v1/telemetry/export — raw recent telemetry-pings + funnel-events
|
|
5155
|
+
// for the unified-revenue-rollup join. Operator-key gated because the raw
|
|
5156
|
+
// event stream may include user-agent strings and other low-PII data
|
|
5157
|
+
// that we never expose unauthenticated. Returns a bounded window
|
|
5158
|
+
// (default last 24h, capped at 10000 rows per stream) so a misbehaving
|
|
5159
|
+
// caller cannot pull the entire local ledger.
|
|
5160
|
+
if (req.method === 'GET' && pathname === '/v1/telemetry/export') {
|
|
5161
|
+
// Strict auth: raw telemetry export exposes user-agent strings and
|
|
5162
|
+
// first-party event payloads. Unlike /v1/billing/summary (which
|
|
5163
|
+
// returns aggregates only), this endpoint must NEVER fall through
|
|
5164
|
+
// to unauthenticated access when both keys happen to be unset —
|
|
5165
|
+
// that would silently turn an unconfigured dev/preview server into
|
|
5166
|
+
// a public ledger reader. Require BOTH a configured key on the
|
|
5167
|
+
// server AND a token on the request that matches one of them.
|
|
5168
|
+
const requestToken = extractApiKey(req);
|
|
5169
|
+
const adminMatches = !!expectedApiKey && requestToken === expectedApiKey;
|
|
5170
|
+
const operatorMatches = !!expectedOperatorKey && requestToken === expectedOperatorKey;
|
|
5171
|
+
const anyKeyConfigured = !!expectedApiKey || !!expectedOperatorKey;
|
|
5172
|
+
if (!anyKeyConfigured) {
|
|
5173
|
+
sendJson(res, 503, {
|
|
5174
|
+
error: 'Telemetry export disabled — neither THUMBGATE_API_KEY nor THUMBGATE_OPERATOR_KEY is configured on this server',
|
|
5175
|
+
});
|
|
5176
|
+
return;
|
|
5177
|
+
}
|
|
5178
|
+
if (!adminMatches && !operatorMatches) {
|
|
5179
|
+
sendJson(res, 401, { error: 'Unauthorized — operator or admin key required' });
|
|
5180
|
+
return;
|
|
5181
|
+
}
|
|
5182
|
+
|
|
5183
|
+
// Reuse the already-parsed URL object from line ~3654 instead of
|
|
5184
|
+
// re-requiring the legacy 'url' module.
|
|
5185
|
+
const params = parsed.searchParams;
|
|
5186
|
+
const sinceRaw = String(params.get('since') || '').trim();
|
|
5187
|
+
const sinceMs = sinceRaw
|
|
5188
|
+
? Date.parse(sinceRaw)
|
|
5189
|
+
: Date.now() - 24 * 60 * 60 * 1000;
|
|
5190
|
+
const since = Number.isFinite(sinceMs) ? sinceMs : Date.now() - 24 * 60 * 60 * 1000;
|
|
5191
|
+
|
|
5192
|
+
const limitRaw = Number(params.get('limit'));
|
|
5193
|
+
const limit = Number.isFinite(limitRaw) && limitRaw > 0
|
|
5194
|
+
? Math.min(Math.floor(limitRaw), 10000)
|
|
5195
|
+
: 1000;
|
|
5196
|
+
|
|
5197
|
+
const sourceRaw = String(params.get('source') || '');
|
|
5198
|
+
const source = ['telemetry', 'funnel', 'both'].includes(sourceRaw) ? sourceRaw : 'both';
|
|
5199
|
+
|
|
5200
|
+
const { FEEDBACK_DIR: exportDir } = getFeedbackPaths();
|
|
5201
|
+
const wantTelemetry = source === 'telemetry' || source === 'both';
|
|
5202
|
+
const wantFunnel = source === 'funnel' || source === 'both';
|
|
5203
|
+
|
|
5204
|
+
const result = {
|
|
5205
|
+
generatedAt: new Date().toISOString(),
|
|
5206
|
+
since: new Date(since).toISOString(),
|
|
5207
|
+
limit,
|
|
5208
|
+
source,
|
|
5209
|
+
telemetry: { rows: [], truncated: false, totalAfterSince: 0 },
|
|
5210
|
+
funnel: { rows: [], truncated: false, totalAfterSince: 0 },
|
|
5211
|
+
};
|
|
5212
|
+
|
|
5213
|
+
function readJsonlSince(p) {
|
|
5214
|
+
try {
|
|
5215
|
+
if (!fs.existsSync(p)) return [];
|
|
5216
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
5217
|
+
const rows = [];
|
|
5218
|
+
for (const line of text.split('\n')) {
|
|
5219
|
+
if (!line) continue;
|
|
5220
|
+
let obj;
|
|
5221
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
5222
|
+
const ts = obj.timestamp || obj.receivedAt || obj.ts || null;
|
|
5223
|
+
const parsed = ts ? Date.parse(ts) : NaN;
|
|
5224
|
+
if (Number.isFinite(parsed) && parsed >= since) {
|
|
5225
|
+
rows.push(obj);
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
return rows;
|
|
5229
|
+
} catch { return []; }
|
|
5230
|
+
}
|
|
5231
|
+
|
|
5232
|
+
if (wantTelemetry) {
|
|
5233
|
+
const tp = path.join(exportDir, 'telemetry-pings.jsonl');
|
|
5234
|
+
const all = readJsonlSince(tp);
|
|
5235
|
+
result.telemetry.totalAfterSince = all.length;
|
|
5236
|
+
result.telemetry.rows = all.slice(-limit);
|
|
5237
|
+
result.telemetry.truncated = all.length > limit;
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
if (wantFunnel) {
|
|
5241
|
+
// Use the canonical funnel ledger path from scripts/billing.js so
|
|
5242
|
+
// any test override (THUMBGATE_FUNNEL_LEDGER_PATH / _TEST_FUNNEL_LEDGER_PATH)
|
|
5243
|
+
// resolves correctly. Falls back to the feedback dir's funnel-events.jsonl.
|
|
5244
|
+
let funnelPath;
|
|
5245
|
+
try {
|
|
5246
|
+
const billing = require('../../scripts/billing');
|
|
5247
|
+
funnelPath = (billing._FUNNEL_LEDGER_PATH && billing._FUNNEL_LEDGER_PATH())
|
|
5248
|
+
|| path.join(exportDir, 'funnel-events.jsonl');
|
|
5249
|
+
} catch {
|
|
5250
|
+
funnelPath = path.join(exportDir, 'funnel-events.jsonl');
|
|
5251
|
+
}
|
|
5252
|
+
const all = readJsonlSince(funnelPath);
|
|
5253
|
+
result.funnel.totalAfterSince = all.length;
|
|
5254
|
+
result.funnel.rows = all.slice(-limit);
|
|
5255
|
+
result.funnel.truncated = all.length > limit;
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
sendJson(res, 200, result);
|
|
5259
|
+
return;
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5051
5262
|
if (req.method === 'OPTIONS' && pathname === '/v1/intake/workflow-sprint') {
|
|
5052
5263
|
sendPublicBillingPreflight(res);
|
|
5053
5264
|
return;
|
|
@@ -5240,6 +5451,222 @@ async function addContext(){
|
|
|
5240
5451
|
return;
|
|
5241
5452
|
}
|
|
5242
5453
|
|
|
5454
|
+
// Public terms of service — required by Stripe / Stripe Checkout for the
|
|
5455
|
+
// "Terms of service URL" field in Business → Public details. Mirrors the
|
|
5456
|
+
// /privacy + /support pages: thin HTML, no external deps, no DB hit.
|
|
5457
|
+
if (isGetLikeRequest && pathname === '/terms') {
|
|
5458
|
+
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Terms of Service — ThumbGate</title></head><body>
|
|
5459
|
+
<h1>Terms of Service</h1>
|
|
5460
|
+
<p><strong>ThumbGate</strong> (npm: thumbgate)</p>
|
|
5461
|
+
<p>Last updated: 2026-05-12</p>
|
|
5462
|
+
<h2>The Service</h2>
|
|
5463
|
+
<p>ThumbGate provides pre-action gates for AI coding agents: a local CLI (MIT-licensed) and an optional hosted tier at thumbgate-production.up.railway.app. By installing the CLI or paying for a subscription, you agree to these terms.</p>
|
|
5464
|
+
<h2>Payment</h2>
|
|
5465
|
+
<p>Paid tiers are billed through Stripe. Subscriptions auto-renew until cancelled. One-off purchases (Sprint Diagnostic, Workflow Sprint, Quick Read, Workflow Teardown, First Failure Rule) are charged once.</p>
|
|
5466
|
+
<h2>Refunds</h2>
|
|
5467
|
+
<p>Pro and Team subscriptions: cancel anytime; we issue a full refund within 7 days of the first charge, prorated thereafter. One-off purchases: refund on request if we cannot deliver the scoped artifact.</p>
|
|
5468
|
+
<h2>Acceptable Use</h2>
|
|
5469
|
+
<p>You may not use ThumbGate to (a) circumvent the safety controls of other AI providers, (b) generate malware or content that violates third-party terms, or (c) resell the hosted tier without written permission.</p>
|
|
5470
|
+
<h2>Disclaimer of Warranty</h2>
|
|
5471
|
+
<p>The service is provided "as is", without warranty of any kind. ThumbGate is a guard rail, not a guarantee. We do not warrant that every AI-agent mistake will be prevented.</p>
|
|
5472
|
+
<h2>Limitation of Liability</h2>
|
|
5473
|
+
<p>Total liability for any claim is limited to the amount you paid in the 12 months preceding the claim.</p>
|
|
5474
|
+
<h2>Governing Law</h2>
|
|
5475
|
+
<p>These terms are governed by the laws of the State of New York, United States.</p>
|
|
5476
|
+
<h2>Changes</h2>
|
|
5477
|
+
<p>We may update these terms; material changes will be announced via the email on file at least 14 days before they take effect.</p>
|
|
5478
|
+
<h2>Contact</h2><p>igor.ganapolsky@gmail.com</p>
|
|
5479
|
+
<p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a> · <a href="/privacy">Privacy</a> · <a href="/support">Support</a></p>
|
|
5480
|
+
</body></html>`, {}, {
|
|
5481
|
+
headOnly: isHeadRequest,
|
|
5482
|
+
});
|
|
5483
|
+
return;
|
|
5484
|
+
}
|
|
5485
|
+
|
|
5486
|
+
// Public case studies — proof surface for buyers. Conversion-optimization
|
|
5487
|
+
// surface that was missing: thumbgate.ai had no /case-studies, so visitors
|
|
5488
|
+
// landed on CLI install commands without seeing whether anyone actually
|
|
5489
|
+
// got value. First entry is the Aiventyx Teams listing integration: real
|
|
5490
|
+
// third-party CTR signal (5/8 clicks before the /go/teams fix, end-to-end
|
|
5491
|
+
// verified after).
|
|
5492
|
+
if (isGetLikeRequest && pathname === '/case-studies') {
|
|
5493
|
+
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
|
+
<h1>Case Studies</h1>
|
|
5495
|
+
<p class="lede">Real integrations. No fabricated logos, no aspirational numbers — every claim below is reproducible.</p>
|
|
5496
|
+
|
|
5497
|
+
<article>
|
|
5498
|
+
<h2>Aiventyx marketplace — Teams listing CTR recovery</h2>
|
|
5499
|
+
<p class="meta">Integration partner: <a href="https://www.aiventyx.com">Aiventyx</a> · Reported by: Qaiser Mehdi · Verified: 2026-05-13</p>
|
|
5500
|
+
|
|
5501
|
+
<h3>The problem</h3>
|
|
5502
|
+
<p>Aiventyx is a marketplace for AI tools. ThumbGate's Teams listing was their highest-CTR external surface — <span class="metric">62% CTR</span> (5 clicks on 8 views, May 7–9 window). When their integrator rolled out canonical tracked URLs, every Teams click started landing on:</p>
|
|
5503
|
+
<p><code>{"error":"Tracked link not found","allowed":["gpt","pro","install","reddit","linkedin","x","github"]}</code></p>
|
|
5504
|
+
<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
|
+
|
|
5506
|
+
<h3>The fix</h3>
|
|
5507
|
+
<p>Added <code>teams</code> to <code>TRACKED_LINK_TARGETS</code>: HTTP 302 redirect to <code>/checkout/pro?plan_id=team&seat_count=3&billing_cycle=monthly</code> — the 3-seat $147/mo self-serve Stripe Team checkout. Caller-supplied UTMs flow through to Stripe metadata end-to-end.</p>
|
|
5508
|
+
|
|
5509
|
+
<h3>The verification</h3>
|
|
5510
|
+
<p>Qaiser's own incognito test, May 13 6:04 AM (full email on record):</p>
|
|
5511
|
+
<p><code>https://thumbgate.ai/go/teams?utm_source=aiventyx</code><br>
|
|
5512
|
+
→ 302 to Stripe checkout<br>
|
|
5513
|
+
→ "Subscribe to ThumbGate Team" page loads<br>
|
|
5514
|
+
→ $147/mo, 3-seat Team plan confirmed<br>
|
|
5515
|
+
→ Aiventyx UTMs intact in URL</p>
|
|
5516
|
+
|
|
5517
|
+
<h3>What this proves</h3>
|
|
5518
|
+
<p>End-to-end attribution from a third-party marketplace through ThumbGate's redirector into Stripe checkout, with the caller's UTM chain preserved. Two regression tests pin the redirect contract so it can't silently break.</p>
|
|
5519
|
+
|
|
5520
|
+
<p><a href="/go/teams?utm_source=case-study">Try the live redirect →</a></p>
|
|
5521
|
+
</article>
|
|
5522
|
+
|
|
5523
|
+
<footer>
|
|
5524
|
+
<p>Want to be the next case study? The product is real, the integration is 30 seconds: <code>npx thumbgate init</code>. If you ship something with ThumbGate and want it documented here, email <a href="mailto:igor.ganapolsky@gmail.com">igor.ganapolsky@gmail.com</a>.</p>
|
|
5525
|
+
<p><a href="https://thumbgate.ai">Home</a> · <a href="/pricing">Pricing</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a> · <a href="/support">Support</a></p>
|
|
5526
|
+
</footer>
|
|
5527
|
+
</body></html>`, {}, {
|
|
5528
|
+
headOnly: isHeadRequest,
|
|
5529
|
+
});
|
|
5530
|
+
return;
|
|
5531
|
+
}
|
|
5532
|
+
|
|
5533
|
+
// Public canonical pricing page. The audit flagged "pricing schizophrenia":
|
|
5534
|
+
// sales/pricing.json said $49 / $299, COMMERCIAL_TRUTH.md said $19 / $149,
|
|
5535
|
+
// and there was no buyer-facing surface to reconcile the two. This is now
|
|
5536
|
+
// the single source of truth for what ThumbGate sells, in priority order:
|
|
5537
|
+
// Sprint (proof-pack, sales-led) → Pro (self-serve recurring) → Team
|
|
5538
|
+
// (after qualification). Every paid CTA across the site should funnel
|
|
5539
|
+
// here OR directly into Stripe checkout — never a different price.
|
|
5540
|
+
if (isGetLikeRequest && pathname === '/pricing') {
|
|
5541
|
+
sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Pricing — ThumbGate</title><meta name="description" content="ThumbGate pricing: free CLI, $19/mo Pro, $49/seat Team, $499 Sprint Diagnostic, $1500 full Workflow Hardening Sprint. Single source of truth across the site."><style>body{font-family:system-ui,-apple-system,sans-serif;max-width:980px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}h1{font-size:36px;margin:0 0 8px;text-align:center}.lede{color:#6b7280;font-size:18px;margin:0 0 32px;text-align:center}.grid{display:grid;grid-template-columns:1fr;gap:20px;margin-bottom:24px}@media(min-width:720px){.grid{grid-template-columns:repeat(2,1fr)}.hero{grid-column:1/-1}}.card{border:1px solid #e5e7eb;border-radius:12px;padding:24px;background:#fff;display:flex;flex-direction:column}.hero{border:2px solid #0f172a;background:linear-gradient(135deg,#fef3c7,#fff)}.tag{display:inline-block;background:#0f172a;color:#fff;padding:3px 10px;border-radius:6px;font-size:12px;font-weight:600;letter-spacing:0.5px;margin-bottom:12px}.tag-free{background:#10b981}.tag-pro{background:#3b82f6}.tag-team{background:#8b5cf6}.tag-diag{background:#0f172a}.tag-sprint{background:#b91c1c}h2{margin:0 0 4px;font-size:22px}.price{font-size:30px;font-weight:700;margin:8px 0 12px}.price small{font-size:14px;color:#6b7280;font-weight:400}.tagline{color:#374151;margin:0 0 16px;font-size:15px}ul{margin:0 0 20px;padding-left:18px}li{margin:6px 0;font-size:14px}.cta{display:inline-block;background:#0f172a;color:#fff;padding:12px 24px;border-radius:8px;font-weight:600;text-decoration:none;text-align:center;margin-top:auto}.cta-secondary{background:#fff;color:#0f172a;border:1px solid #0f172a}.cta-free{background:#10b981}.micro{border-top:1px solid #e5e7eb;padding-top:24px;margin-top:16px}.micro-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin:16px 0}.micro-card{border:1px solid #e5e7eb;border-radius:8px;padding:14px;text-align:center;background:#fafafa}.micro-card .mp{font-size:20px;font-weight:700;color:#0f172a}.micro-card .ml{font-size:13px;color:#6b7280;margin:4px 0 8px}.micro-card a{color:#0066cc;font-size:13px;text-decoration:none}footer{margin-top:32px;padding-top:24px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px;text-align:center}footer a{color:#0066cc}</style></head><body>
|
|
5542
|
+
<h1>Pricing</h1>
|
|
5543
|
+
<p class="lede">Six paths to ThumbGate. Pick by what you need: an install, a subscription, a team rollout, or a stakeholder-visible artifact.</p>
|
|
5544
|
+
|
|
5545
|
+
<div class="grid">
|
|
5546
|
+
|
|
5547
|
+
<div class="card hero">
|
|
5548
|
+
<span class="tag tag-sprint">Sprint — Full engagement</span>
|
|
5549
|
+
<h2>Workflow Hardening Sprint</h2>
|
|
5550
|
+
<div class="price">$1,500 <small>· one-time</small></div>
|
|
5551
|
+
<p class="tagline">The full hardening engagement for a single AI-agent workflow. Built on top of the Sprint Diagnostic — we apply the diagnostic findings into a shipped Pre-Action Check pack, deploy hooks into your repo, and run the rollout review. Two weeks calendar-time, single fixed price.</p>
|
|
5552
|
+
<ul>
|
|
5553
|
+
<li>Diagnostic + applied rules + deployed PreToolUse hooks</li>
|
|
5554
|
+
<li>Best path if you need the workflow actually fixed, not just diagnosed</li>
|
|
5555
|
+
<li>Refund if we can't extract or apply a rule</li>
|
|
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
|
+
});
|
|
5643
|
+
return;
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5646
|
+
// Public support / contact page — required for Stripe Business → Public
|
|
5647
|
+
// details "Customer support URL" field. Single source of truth for how
|
|
5648
|
+
// customers reach us (email, GitHub issues, status page).
|
|
5649
|
+
if (isGetLikeRequest && pathname === '/support') {
|
|
5650
|
+
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Support — ThumbGate</title></head><body>
|
|
5651
|
+
<h1>Support</h1>
|
|
5652
|
+
<p><strong>ThumbGate</strong> support, billing, and contact paths.</p>
|
|
5653
|
+
<h2>Email</h2>
|
|
5654
|
+
<p>For billing questions, refunds, subscription changes, or technical issues with the hosted tier: <a href="mailto:igor.ganapolsky@gmail.com">igor.ganapolsky@gmail.com</a>. We reply within one business day.</p>
|
|
5655
|
+
<h2>GitHub Issues</h2>
|
|
5656
|
+
<p>For bugs, CLI questions, and feature requests in the open-source CLI: <a href="https://github.com/IgorGanapolsky/ThumbGate/issues">github.com/IgorGanapolsky/ThumbGate/issues</a>.</p>
|
|
5657
|
+
<h2>Status</h2>
|
|
5658
|
+
<p>Hosted-tier status: check <a href="https://thumbgate-production.up.railway.app/health">/health</a> for current health. Railway-hosted; rebuilds take 2-5 minutes.</p>
|
|
5659
|
+
<h2>Refunds</h2>
|
|
5660
|
+
<p>Pro / Team subscriptions: 7-day full refund window from first charge. One-off purchases: refund on request if we cannot deliver. Email the address above.</p>
|
|
5661
|
+
<h2>Security</h2>
|
|
5662
|
+
<p>Disclose vulnerabilities by email; please do not post to public GitHub issues. We acknowledge within 48 hours.</p>
|
|
5663
|
+
<p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a></p>
|
|
5664
|
+
</body></html>`, {}, {
|
|
5665
|
+
headOnly: isHeadRequest,
|
|
5666
|
+
});
|
|
5667
|
+
return;
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5243
5670
|
// Public privacy policy — required for GPT Store and marketplace listings
|
|
5244
5671
|
if (isGetLikeRequest && pathname === '/privacy') {
|
|
5245
5672
|
sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Privacy Policy — ThumbGate</title></head><body>
|