VinOS Agent Claude Opus 4.6 commited on
Commit
6144942
Β·
1 Parent(s): 42a1221

Phase 0+6+7: Quick Wins, SEO-to-Revenue Pipeline, Self-Optimization

Browse files

Phase 0: Fix _handleWrite/_handleResearch crash bugs, wire /costs,
/scout, /landing commands, create link-in-bio landing page.

Phase 6: CTA auto-injection in WordPress articles with UTM tracking,
/seo2offer one-command pipeline (scout→offer→article→publish→index),
post-publish offer suggestion.

Phase 7: A/B variant model per offer, /offer ab and /offer variants
commands, smart evaluation (auto-variant at 48h, pause at 72h, scale
winners), click tracking at /api/track, failure pattern learning from
retired offers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

.claude/claude.md CHANGED
@@ -8,3 +8,13 @@ Your job is to maintain, expand, and execute the VinOS local stack using Node.js
8
  2. Use `skills/api_caller.js` for external integrations.
9
  3. Update `public/index.html` and `public/app.js` when adding new dashboard features.
10
  4. If a route is missing, add it to `server.js`.
 
 
 
 
 
 
 
 
 
 
 
8
  2. Use `skills/api_caller.js` for external integrations.
9
  3. Update `public/index.html` and `public/app.js` when adding new dashboard features.
10
  4. If a route is missing, add it to `server.js`.
11
+
12
+ ## Deployment Protocol:
13
+ After every commit that completes a phase or significant feature:
14
+ 1. **Push to HF Spaces** β€” `git push hf master:main` (deploys to live `AIgoose/vinos-engine`)
15
+ 2. **Send Telegram deploy notification** β€” Run `node -e "require('./skills/hf_deployer').notifyDeploy()"` after push. This generates AI release notes from the commits and sends a notification to Telegram with what's new.
16
+ 3. **Update CHANGELOG.md** β€” Add a dated entry at the top with: phase name, commit hash, what changed (files created/modified, new commands, new endpoints, new dashboards)
17
+ 4. **Verify deployment** β€” Boot test (`node -e "require('./server.js')"`) or check HF Space logs
18
+ 5. Never leave unpushed commits β€” if code is committed, it should be deployed unless explicitly told otherwise
19
+
20
+ Alternative: Use `hfDeployer.syncCode()` which handles push + notification together (used by `/sync` Telegram command).
CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
 
3
  ---
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  ## Phase 5 β€” Autonomous Traffic-to-Sales Engine
6
  **Date:** 2026-03-27 | **Commit:** `f30cee0`
7
 
 
2
 
3
  ---
4
 
5
+ ## Phase 0 + 6 + 7 β€” Quick Wins, SEO-to-Revenue, Self-Optimization
6
+ **Date:** 2026-03-27
7
+
8
+ ### Phase 0: Quick Wins + Bug Fix
9
+ - **Fixed `_handleWrite` crash** β€” Defined missing `_handleWrite()` and `_handleResearch()` functions. The `/write` flow no longer crashes when user replies with context.
10
+ - **`/costs` command** β€” Shows API spend breakdown by model from `cost_tracker.getCosts()`
11
+ - **`/scout [keyword]` command** β€” Market viability scout (competition, volume, angles)
12
+ - **`/landing` page** β€” Public link-in-bio page at `/landing` auto-renders active offers with Mayar CTA buttons
13
+ - **`/landing` command** β€” Returns the public URL for sharing
14
+ - Telegram menu updated with `/costs`, `/scout`, `/seo2offer`, `/landing`
15
+
16
+ ### Phase 6: SEO-to-Revenue Pipeline
17
+ - **CTA injection in WordPress publisher** β€” Articles auto-inject a styled CTA block when a matching offer exists, with UTM tracking (`utm_source=seo&utm_medium=article&utm_campaign=...`)
18
+ - **UTM support in `injectOfferLink()`** β€” Returns `utmUrl` alongside `url` for attribution tracking across SEO articles vs social posts
19
+ - **`/seo2offer [keyword]` command** β€” One-command full pipeline: scout β†’ create offer β†’ write SEO article (with auto-CTA) β†’ publish to WordPress β†’ Google index
20
+ - **Post-publish offer suggestion** β€” After successful WP publish, if no matching offer exists, Telegram suggests `/offer [topic]`
21
+
22
+ ### Phase 7: Self-Optimization & A/B Testing
23
+ - **Offer variants model** β€” Each offer now supports a `variants[]` array with per-variant stats (impressions, clicks, sales, isActive)
24
+ - **`createVariant(offerId)`** β€” AI generates alternative copy/angle, informed by failure patterns
25
+ - **Smart evaluation** β€” Replaces binary pause/scale: 48h no sales + <2 variants β†’ auto-create variant; 72h still 0 β†’ pause; 3+ sales/24h β†’ scale winner + pause loser variants
26
+ - **`/offer ab [id]` command** β€” Create A/B test variant for any offer
27
+ - **`/offer variants [id]` command** β€” View all variants with conversion rates
28
+ - **Click tracking** β€” `GET /api/track?offer=X&variant=Y` increments counters and redirects to Mayar
29
+ - **Failure pattern learning** β€” `analyzeFailurePatterns()` extracts patterns from retired offers via LLM, stores in `failure_patterns`, future offer creation avoids repeating mistakes
30
+
31
+ #### NEW FILES
32
+ | File | Purpose |
33
+ |------|---------|
34
+ | `public/landing.html` | Link-in-bio page β€” dark theme, auto-renders active offers from `/api/landing` |
35
+
36
+ #### MODIFIED FILES
37
+ | File | Changes |
38
+ |------|---------|
39
+ | `server.js` | `_handleWrite()`, `_handleResearch()` functions, `/costs`, `/scout`, `/landing`, `/seo2offer` commands, `/api/track` click tracking, `/api/landing` endpoint, `/offer ab`, `/offer variants` |
40
+ | `skills/sales_engine.js` | `createVariant()`, `getVariants()`, `trackClick()`, `analyzeFailurePatterns()`, smart `evaluateOffers()`, UTM-aware `injectOfferLink()` |
41
+ | `skills/wordpress_publisher.js` | CTA block injection with offer matching before `finalContent` assembly |
42
+ | `skills/set_telegram_menu.js` | Added `/costs`, `/scout`, `/seo2offer`, `/landing` |
43
+
44
+ ---
45
+
46
  ## Phase 5 β€” Autonomous Traffic-to-Sales Engine
47
  **Date:** 2026-03-27 | **Commit:** `f30cee0`
48
 
public/landing.html ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VinOS β€” Digital Products</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
+ <style>
10
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11
+ :root {
12
+ --bg: #090c12; --surface: #111622; --surface2: #161e2e; --border: #1e2d45;
13
+ --accent: #00e5ff; --accent2: #7c3aed; --green: #22d3a0; --orange: #f59e0b;
14
+ --text: #e2e8f0; --muted: #64748b;
15
+ }
16
+ body { font-family: 'Space Grotesk', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; padding: 20px; }
17
+ .container { max-width: 600px; margin: 0 auto; }
18
+ .brand { text-align: center; padding: 40px 0 30px; }
19
+ .brand h1 { font-size: 2rem; font-weight: 700; background: linear-gradient(135deg, var(--accent), var(--green)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
20
+ .brand p { color: var(--muted); margin-top: 8px; font-size: 0.9rem; }
21
+ .offers-grid { display: flex; flex-direction: column; gap: 16px; margin-bottom: 40px; }
22
+ .offer-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 24px; transition: transform 0.2s, box-shadow 0.2s; }
23
+ .offer-card:hover { transform: translateY(-2px); box-shadow: 0 0 30px rgba(0, 229, 255, 0.08); }
24
+ .offer-card .pillar { color: var(--accent); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 600; margin-bottom: 8px; }
25
+ .offer-card h2 { font-size: 1.2rem; font-weight: 600; margin-bottom: 8px; }
26
+ .offer-card .desc { color: var(--muted); font-size: 0.85rem; margin-bottom: 16px; line-height: 1.5; }
27
+ .offer-card .price { font-size: 1.4rem; font-weight: 700; color: var(--green); margin-bottom: 16px; }
28
+ .offer-card .cta { display: inline-block; padding: 12px 28px; background: linear-gradient(135deg, var(--accent), var(--green)); color: #000; font-weight: 700; font-size: 0.9rem; border-radius: 10px; text-decoration: none; transition: opacity 0.2s; }
29
+ .offer-card .cta:hover { opacity: 0.9; }
30
+ .empty { text-align: center; color: var(--muted); padding: 60px 20px; font-style: italic; }
31
+ .footer { text-align: center; padding: 30px 0; border-top: 1px solid var(--border); }
32
+ .footer p { color: var(--muted); font-size: 0.75rem; }
33
+ .footer .links { margin-top: 12px; display: flex; justify-content: center; gap: 16px; }
34
+ .footer .links a { color: var(--accent); text-decoration: none; font-size: 0.8rem; }
35
+ .footer .links a:hover { text-decoration: underline; }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <div class="container">
40
+ <div class="brand">
41
+ <h1>VinOS</h1>
42
+ <p>AI-Powered Digital Products</p>
43
+ </div>
44
+ <div class="offers-grid" id="offersGrid">
45
+ <div class="empty">Loading offers...</div>
46
+ </div>
47
+ <div class="footer">
48
+ <p>Powered by VinOS Engine</p>
49
+ </div>
50
+ </div>
51
+ <script>
52
+ const esc = s => { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
53
+ const fmt = n => `Rp ${(n || 0).toLocaleString('id-ID')}`;
54
+
55
+ async function loadOffers() {
56
+ try {
57
+ const res = await fetch('/api/landing');
58
+ const data = await res.json();
59
+ const grid = document.getElementById('offersGrid');
60
+
61
+ if (!data.success || !data.offers || data.offers.length === 0) {
62
+ grid.innerHTML = '<div class="empty">No offers available right now. Check back soon!</div>';
63
+ return;
64
+ }
65
+
66
+ let html = '';
67
+ for (const o of data.offers) {
68
+ html += `<div class="offer-card">
69
+ ${o.pillar ? `<div class="pillar">${esc(o.pillar)}</div>` : ''}
70
+ <h2>${esc(o.title)}</h2>
71
+ ${o.description ? `<div class="desc">${esc(o.description)}</div>` : ''}
72
+ <div class="price">${fmt(o.priceIDR)}</div>
73
+ ${o.paymentUrl ? `<a class="cta" href="${esc(o.paymentUrl)}" target="_blank">Get It Now &rarr;</a>` : '<span class="desc">Coming soon</span>'}
74
+ </div>`;
75
+ }
76
+ grid.innerHTML = html;
77
+ } catch (e) {
78
+ document.getElementById('offersGrid').innerHTML = '<div class="empty">Unable to load offers.</div>';
79
+ }
80
+ }
81
+
82
+ loadOffers();
83
+ </script>
84
+ </body>
85
+ </html>
server.js CHANGED
@@ -57,6 +57,8 @@ const analyticsEngine = require('./analytics-engine');
57
  const viralFlow = require('./skills/viral_flow');
58
  const salesEngine = require('./skills/sales_engine');
59
  const mayarClient = require('./skills/mayar_client');
 
 
60
 
61
 
62
 
@@ -80,6 +82,21 @@ app.get('/revenue', (req, res) => {
80
  res.sendFile(path.join(__dirname, 'public', 'revenue-dashboard.html'));
81
  });
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  // ============================================
84
  // SALES ENGINE API
85
  // ============================================
@@ -136,6 +153,19 @@ app.post('/api/sales/webhook/mayar', async (req, res) => {
136
  }
137
  });
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  // ============================================
140
  // VIRAL CONTENT ENGINE API
141
  // ============================================
@@ -1048,6 +1078,131 @@ async function processOrchestratorIntent(chatId, intent, userText, params) {
1048
  }
1049
  }
1050
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
  // Telegram Webhook
1052
  app.post('/api/telegram-webhook', async (req, res) => {
1053
  res.status(200).send({ status: 'received' });
@@ -1727,14 +1882,22 @@ This engine researches any URL or topic and generates content with your options.
1727
  `πŸ“ <b>SEO ENGINE</b>\n` +
1728
  `/seo [keyword] β€” Architect SEO Pillar Strategy\n` +
1729
  `/write [siteId] [size] [topic] β€” Publish SEO Article\n` +
 
1730
  `/index [url] β€” Submit to Google API\n\n` +
1731
  `πŸ’° <b>SALES ENGINE</b>\n` +
1732
  `/offer β€” List active offers with payment links\n` +
1733
  `/offer [topic] β€” Create offer (AI copy β†’ Mayar link)\n` +
1734
  `/offer pause [id] β€” Pause underperforming offer\n` +
 
 
1735
  `/offer board β€” Pipeline stage counts\n` +
 
1736
  `/revenue β€” Revenue summary\n` +
1737
  `/revenue today β€” Today's sales only\n\n` +
 
 
 
 
1738
  `πŸ“‹ <b>CRM MANAGER</b>\n` +
1739
  `/updates – View Trello project statuses\n` +
1740
  `/move [4-char-ID] [new list] – Update card status`;
@@ -1818,6 +1981,65 @@ This engine researches any URL or topic and generates content with your options.
1818
  await apiCaller.sendTelegramMessage(chatId, `❌ Image generation failed: ${gashResult.error || 'All providers exhausted'}`);
1819
  }
1820
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1821
  } else if (userText.toLowerCase().startsWith('/seo')) {
1822
  const keyword = userText.split(' ').slice(1).join(' ');
1823
  if (!keyword) {
@@ -2084,6 +2306,27 @@ This engine researches any URL or topic and generates content with your options.
2084
  const emoji = { active: '🟒', pending_link: '🟑', pending_validation: 'πŸ”΅', paused: '⏸️', retired: 'πŸ”΄' };
2085
  const lines = stages.filter(s => counts[s]).map(s => `${emoji[s] || 'β€’'} ${s}: ${counts[s]}`).join('\n');
2086
  await apiCaller.sendTelegramMessage(chatId, `πŸ“‹ <b>Offer Pipeline</b>\n\n${lines || 'No offers yet.'}\n\nTotal: ${db.offers.length}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2087
  } else if (args.toLowerCase().startsWith('eval')) {
2088
  // /offer eval β€” run evaluation
2089
  await apiCaller.sendTelegramMessage(chatId, 'πŸ“Š <i>Evaluating offers...</i>');
@@ -2136,6 +2379,35 @@ This engine researches any URL or topic and generates content with your options.
2136
  );
2137
  }
2138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2139
  } else {
2140
  await handleVinIntent(chatId, from, userText);
2141
  }
 
57
  const viralFlow = require('./skills/viral_flow');
58
  const salesEngine = require('./skills/sales_engine');
59
  const mayarClient = require('./skills/mayar_client');
60
+ const costTracker = require('./skills/cost_tracker');
61
+ const scoutAgent = require('./skills/scout_agent');
62
 
63
 
64
 
 
82
  res.sendFile(path.join(__dirname, 'public', 'revenue-dashboard.html'));
83
  });
84
 
85
+ app.get('/landing', (req, res) => {
86
+ res.sendFile(path.join(__dirname, 'public', 'landing.html'));
87
+ });
88
+
89
+ app.get('/api/landing', (req, res) => {
90
+ try {
91
+ const dash = salesEngine.getDashboardData();
92
+ const activeOffers = dash.activeOffers.map(o => ({
93
+ title: o.title, description: o.description || '',
94
+ priceIDR: o.priceIDR, paymentUrl: o.paymentUrl, pillar: o.pillar || ''
95
+ }));
96
+ res.json({ success: true, offers: activeOffers });
97
+ } catch (e) { res.json({ success: true, offers: [] }); }
98
+ });
99
+
100
  // ============================================
101
  // SALES ENGINE API
102
  // ============================================
 
153
  }
154
  });
155
 
156
+ // Click tracking redirect (for A/B variant attribution)
157
+ app.get('/api/track', (req, res) => {
158
+ const { offer, variant, action } = req.query;
159
+ if (!offer) return res.status(400).json({ error: 'Missing offer param' });
160
+ const redirectUrl = salesEngine.trackClick(offer, variant || 'v0');
161
+ if (redirectUrl) {
162
+ const utm = `${redirectUrl.includes('?') ? '&' : '?'}utm_source=track&utm_medium=cta&utm_campaign=${encodeURIComponent(offer)}`;
163
+ res.redirect(302, redirectUrl + utm);
164
+ } else {
165
+ res.status(404).json({ error: 'Offer not found' });
166
+ }
167
+ });
168
+
169
  // ============================================
170
  // VIRAL CONTENT ENGINE API
171
  // ============================================
 
1078
  }
1079
  }
1080
 
1081
+ // --- Helper: handle /write context reply ---
1082
+ async function _handleWrite(chatId, pending, userContext) {
1083
+ const { targetSiteId, topic, size, pov, intent, tone } = pending;
1084
+
1085
+ const initRes = await apiCaller.sendTelegramMessage(chatId, `πŸ“ <b>SEO Engine Pipeline</b>\n[β– β–‘β–‘β–‘β–‘β–‘] 16% AI Architecting: "${topic}"...`);
1086
+ const msgId = initRes.messageId;
1087
+
1088
+ try {
1089
+ const articleRes = await seoWriter.generateArticle(size, pov, intent, tone, topic, userContext);
1090
+ if (!articleRes.success) {
1091
+ if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `⚠️ <b>Pipeline Failed</b>\n❌ Writer Error: ${articleRes.error}`);
1092
+ return;
1093
+ }
1094
+
1095
+ if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `πŸ“ <b>SEO Engine Pipeline</b>\n[β– β– β–‘β–‘β–‘β–‘] 33% QC: Scoring EEAT Quality...`);
1096
+ const scoreRes = await seoWriter.scoreEEAT(articleRes.html);
1097
+
1098
+ if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `πŸ“ <b>SEO Engine Pipeline</b>\n[β– β– β– β–‘β–‘β–‘] 50% Image Engine: Generating ${articleRes.imagePlaceholders?.length || 0} AI Images...`);
1099
+
1100
+ if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `πŸ“ <b>SEO Engine Pipeline</b>\n[β– β– β– β– β–‘β–‘] 66% Publishing: Uploading to [${targetSiteId}]...`);
1101
+ const pubRes = await wordpressPublisher.publishPost(articleRes, targetSiteId);
1102
+
1103
+ if (!pubRes.success) {
1104
+ if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `⚠️ <b>Pipeline Failed</b>\n❌ WordPress Error: ${pubRes.error}`);
1105
+ return;
1106
+ }
1107
+
1108
+ if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `πŸ“ <b>SEO Engine Pipeline</b>\n[β– β– β– β– β– β–‘] 83% SEO Indexing: Google Search Console...`);
1109
+ const idxRes = await googleIndexer.submitUrl(pubRes.url);
1110
+
1111
+ if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `πŸ“ <b>SEO Engine Pipeline</b>\n[β– β– β– β– β– β– ] 100% CRM Sync: Trello Workflow...`);
1112
+ const trelloRes = await trelloManager.logSeoPost(topic, targetSiteId || 'Default', pubRes.url, scoreRes.score || 'N/A');
1113
+
1114
+ let finalMsg = `🌟 <b>Pipeline Complete!</b>\n\n`;
1115
+ finalMsg += `<b>πŸ”— Article:</b> ${pubRes.url}\n`;
1116
+ if (scoreRes.success) finalMsg += `<b>🧠 EEAT Score:</b> ${scoreRes.score}/100\n`;
1117
+ if (idxRes.success) finalMsg += `<b>πŸ” Google:</b> Indexed 🟒\n`;
1118
+ else finalMsg += `<b>πŸ” Google:</b> Pending 🟑 (${idxRes.error})\n`;
1119
+ if (trelloRes.success) finalMsg += `<b>πŸ“Š Trello:</b> <a href="${trelloRes.url}">View Card</a> πŸ“‹\n`;
1120
+ else finalMsg += `<b>πŸ“Š Trello:</b> Failed πŸ”΄\n`;
1121
+
1122
+ // Check for matching sales offer β†’ suggest creating one if none
1123
+ try {
1124
+ const offerLink = salesEngine.injectOfferLink(topic);
1125
+ if (offerLink) {
1126
+ finalMsg += `\nπŸ’° <b>Offer CTA injected:</b> ${offerLink}\n`;
1127
+ } else {
1128
+ finalMsg += `\nπŸ’‘ No matching offer for "${topic}". Create one? <code>/offer ${topic}</code>\n`;
1129
+ }
1130
+ } catch (e) { /* non-critical */ }
1131
+
1132
+ await apiCaller.sendTelegramMessage(chatId, finalMsg);
1133
+
1134
+ // Auto-push SEO publish metadata to HF
1135
+ try {
1136
+ await hfStorage.saveRecord('seo', `seo_${Date.now()}`, {
1137
+ title: `SEO: ${topic}`,
1138
+ timestamp: new Date().toISOString(),
1139
+ source_url: pubRes.url,
1140
+ niche: 'SEO'
1141
+ }, `# Published: ${topic}\n\n- URL: ${pubRes.url}\n- Site: ${targetSiteId || 'Default'}\n- EEAT Score: ${scoreRes.score || 'N/A'}\n- Google Indexed: ${idxRes.success}\n- Trello: ${trelloRes.success ? trelloRes.url : 'N/A'}`);
1142
+ } catch (e) {
1143
+ console.error('[HF Auto] SEO log failed:', e.message);
1144
+ }
1145
+ } catch (err) {
1146
+ console.error('[_handleWrite] Error:', err.message);
1147
+ await apiCaller.sendTelegramMessage(chatId, `⚠️ <b>Write Pipeline Error:</b> ${err.message}`);
1148
+ }
1149
+ }
1150
+
1151
+ // --- Helper: handle /research context reply ---
1152
+ async function _handleResearch(chatId, url, angle) {
1153
+ await apiCaller.sendTelegramMessage(chatId, `πŸ” <b>Research Auto-Pilot</b>\nAnalyzing URL with focus: <i>${angle || 'General'}</i>...`);
1154
+
1155
+ const researchModule = require('./research');
1156
+ const aiContentMod = require('./ai-content');
1157
+ const imageGenMod = require('./image-gen');
1158
+ const schedulerMod = require('./scheduler');
1159
+
1160
+ try {
1161
+ const scrapeRes = await researchModule.scrapePost(url, angle);
1162
+ if (!scrapeRes.success) {
1163
+ await apiCaller.sendTelegramMessage(chatId, `❌ Scrape failed: ${scrapeRes.error}`);
1164
+ return;
1165
+ }
1166
+
1167
+ await apiCaller.sendTelegramMessage(chatId, `🧠 Scrape successful! Remixing content...`);
1168
+ const remixRes = await aiContentMod.remix(scrapeRes.data);
1169
+ if (!remixRes.success) {
1170
+ await apiCaller.sendTelegramMessage(chatId, `❌ AI Remix failed: ${remixRes.error}`);
1171
+ return;
1172
+ }
1173
+
1174
+ const v1 = remixRes.data.variant_1;
1175
+ await apiCaller.sendTelegramMessage(chatId, `🎨 Generating high-conversion visual...`);
1176
+ const imgRes = await imageGenMod.generateAndUpload(v1.visual_prompt, 'res_v1');
1177
+ if (imgRes.success) v1.mediaUrl = imgRes.publicUrl;
1178
+
1179
+ const draftId = `res_${Date.now().toString().slice(-6)}`;
1180
+ const draftPost = {
1181
+ id: draftId, status: 'draft', created: new Date().toISOString(),
1182
+ input: url, platform: scrapeRes.data.platform || 'web',
1183
+ sourceData: scrapeRes.data, type: 'standard',
1184
+ v1, v2: remixRes.data.variant_2, v3: remixRes.data.variant_3
1185
+ };
1186
+ schedulerMod.config.posts.push(draftPost);
1187
+ schedulerMod.saveDB();
1188
+
1189
+ if (imgRes.success && v1.mediaUrl) {
1190
+ const FormData = require('form-data');
1191
+ const fForm = new FormData();
1192
+ fForm.append('chat_id', chatId);
1193
+ fForm.append('photo', v1.mediaUrl.startsWith('/') ? fs.createReadStream(path.join(__dirname, 'public', v1.mediaUrl)) : v1.mediaUrl);
1194
+ fForm.append('caption', `βœ… <b>Research Draft Ready!</b>\n\n${v1.ig_en.substring(0, 800)}...\n\nReview in Dashboard.`);
1195
+ fForm.append('parse_mode', 'HTML');
1196
+ await apiCaller.axiosIPv4.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendPhoto`, fForm, { headers: fForm.getHeaders() });
1197
+ } else {
1198
+ await apiCaller.sendTelegramMessage(chatId, `βœ… <b>Research Draft Ready!</b>\n\n${v1.ig_en.substring(0, 800)}...\n\nReview in Dashboard.`);
1199
+ }
1200
+ } catch (err) {
1201
+ console.error('[_handleResearch] Error:', err.message);
1202
+ await apiCaller.sendTelegramMessage(chatId, `⚠️ <b>Research Error:</b> ${err.message}`);
1203
+ }
1204
+ }
1205
+
1206
  // Telegram Webhook
1207
  app.post('/api/telegram-webhook', async (req, res) => {
1208
  res.status(200).send({ status: 'received' });
 
1882
  `πŸ“ <b>SEO ENGINE</b>\n` +
1883
  `/seo [keyword] β€” Architect SEO Pillar Strategy\n` +
1884
  `/write [siteId] [size] [topic] β€” Publish SEO Article\n` +
1885
+ `/seo2offer [keyword] β€” Full pipeline: scout β†’ offer β†’ article β†’ publish β†’ index\n` +
1886
  `/index [url] β€” Submit to Google API\n\n` +
1887
  `πŸ’° <b>SALES ENGINE</b>\n` +
1888
  `/offer β€” List active offers with payment links\n` +
1889
  `/offer [topic] β€” Create offer (AI copy β†’ Mayar link)\n` +
1890
  `/offer pause [id] β€” Pause underperforming offer\n` +
1891
+ `/offer ab [id] β€” Create A/B test variant\n` +
1892
+ `/offer variants [id] β€” View variant stats\n` +
1893
  `/offer board β€” Pipeline stage counts\n` +
1894
+ `/offer eval β€” Run offer evaluation\n` +
1895
  `/revenue β€” Revenue summary\n` +
1896
  `/revenue today β€” Today's sales only\n\n` +
1897
+ `πŸ” <b>RESEARCH</b>\n` +
1898
+ `/scout [keyword] β€” Market viability scout\n` +
1899
+ `/costs β€” API spend breakdown\n` +
1900
+ `/landing β€” Your link-in-bio page\n\n` +
1901
  `πŸ“‹ <b>CRM MANAGER</b>\n` +
1902
  `/updates – View Trello project statuses\n` +
1903
  `/move [4-char-ID] [new list] – Update card status`;
 
1981
  await apiCaller.sendTelegramMessage(chatId, `❌ Image generation failed: ${gashResult.error || 'All providers exhausted'}`);
1982
  }
1983
  }
1984
+ } else if (userText.toLowerCase().startsWith('/seo2offer')) {
1985
+ const keyword = userText.split(' ').slice(1).join(' ').trim();
1986
+ if (!keyword) {
1987
+ await apiCaller.sendTelegramMessage(chatId, `❌ Usage: <code>/seo2offer [keyword]</code>\nFull pipeline: scout β†’ offer β†’ article β†’ publish β†’ index`);
1988
+ return;
1989
+ }
1990
+ await apiCaller.sendTelegramMessage(chatId, `πŸš€ <b>SEO-to-Revenue Pipeline</b>\n[β– β–‘β–‘β–‘β–‘β–‘] Scouting: "${keyword}"...`);
1991
+
1992
+ // 1. Scout market viability
1993
+ let scoutResult;
1994
+ try { scoutResult = await scoutAgent.runScout(keyword); } catch (e) { scoutResult = 'Scout unavailable'; }
1995
+
1996
+ await apiCaller.sendTelegramMessage(chatId, `πŸš€ <b>SEO-to-Revenue Pipeline</b>\n[β– β– β–‘β–‘β–‘β–‘] Creating offer + Mayar link...`);
1997
+
1998
+ // 2. Create offer
1999
+ const offerResult = await salesEngine.createOfferFromTopic(keyword, 49000);
2000
+ let offerMsg = '';
2001
+ if (offerResult.success) {
2002
+ offerMsg = `βœ… Offer: ${offerResult.offer.title}\nπŸ’³ ${offerResult.offer.paymentUrl || 'Pending'}`;
2003
+ } else {
2004
+ offerMsg = `⚠️ Offer: ${offerResult.verdict || 'skipped'} (${offerResult.reason || offerResult.error || ''})`;
2005
+ }
2006
+
2007
+ await apiCaller.sendTelegramMessage(chatId, `πŸš€ <b>SEO-to-Revenue Pipeline</b>\n[β– β– β– β–‘β–‘β–‘] Writing SEO article...`);
2008
+
2009
+ // 3. Write article
2010
+ let sites = {};
2011
+ try { sites = JSON.parse(process.env.WP_SITES_JSON || "{}"); } catch(e){}
2012
+ const siteKeys = Object.keys(sites);
2013
+ const targetSite = siteKeys.length > 0 ? siteKeys[0] : null;
2014
+
2015
+ const articleRes = await seoWriter.generateArticle('medium', '3rd person', 'Educational', 'Authoritative', keyword, '');
2016
+ if (!articleRes.success) {
2017
+ await apiCaller.sendTelegramMessage(chatId, `⚠️ Article failed: ${articleRes.error}\n\n${offerMsg}\n\nπŸ•΅οΈ <b>Scout:</b>\n${scoutResult}`);
2018
+ return;
2019
+ }
2020
+
2021
+ await apiCaller.sendTelegramMessage(chatId, `πŸš€ <b>SEO-to-Revenue Pipeline</b>\n[β– β– β– β– β–‘β–‘] Publishing to WordPress (CTA auto-injected)...`);
2022
+
2023
+ // 4. Publish (CTA injection happens inside publisher automatically)
2024
+ const pubRes = await wordpressPublisher.publishPost(articleRes, targetSite);
2025
+ if (!pubRes.success) {
2026
+ await apiCaller.sendTelegramMessage(chatId, `⚠️ Publish failed: ${pubRes.error}\n\n${offerMsg}\n\nπŸ•΅οΈ <b>Scout:</b>\n${scoutResult}`);
2027
+ return;
2028
+ }
2029
+
2030
+ await apiCaller.sendTelegramMessage(chatId, `πŸš€ <b>SEO-to-Revenue Pipeline</b>\n[β– β– β– β– β– β–‘] Indexing on Google...`);
2031
+
2032
+ // 5. Index
2033
+ const idxRes = await googleIndexer.submitUrl(pubRes.url);
2034
+
2035
+ // 6. Final report
2036
+ let finalMsg = `🎯 <b>SEO-to-Revenue Complete!</b>\n\n`;
2037
+ finalMsg += `πŸ”— <b>Article:</b> ${pubRes.url}\n`;
2038
+ finalMsg += `πŸ” <b>Google:</b> ${idxRes.success ? 'Indexed 🟒' : 'Pending 🟑'}\n`;
2039
+ finalMsg += `${offerMsg}\n\n`;
2040
+ finalMsg += `πŸ•΅οΈ <b>Scout Report:</b>\n${typeof scoutResult === 'string' ? scoutResult.substring(0, 500) : 'N/A'}`;
2041
+ await apiCaller.sendTelegramMessage(chatId, finalMsg);
2042
+
2043
  } else if (userText.toLowerCase().startsWith('/seo')) {
2044
  const keyword = userText.split(' ').slice(1).join(' ');
2045
  if (!keyword) {
 
2306
  const emoji = { active: '🟒', pending_link: '🟑', pending_validation: 'πŸ”΅', paused: '⏸️', retired: 'πŸ”΄' };
2307
  const lines = stages.filter(s => counts[s]).map(s => `${emoji[s] || 'β€’'} ${s}: ${counts[s]}`).join('\n');
2308
  await apiCaller.sendTelegramMessage(chatId, `πŸ“‹ <b>Offer Pipeline</b>\n\n${lines || 'No offers yet.'}\n\nTotal: ${db.offers.length}`);
2309
+ } else if (args.toLowerCase().startsWith('ab ')) {
2310
+ // /offer ab [id] β€” create A/B variant
2311
+ const offerId = args.split(' ')[1];
2312
+ await apiCaller.sendTelegramMessage(chatId, `πŸ§ͺ <i>Creating variant for ${offerId}...</i>`);
2313
+ const result = await salesEngine.createVariant(offerId);
2314
+ if (!result.success) {
2315
+ await apiCaller.sendTelegramMessage(chatId, `❌ ${result.error}`);
2316
+ }
2317
+ } else if (args.toLowerCase().startsWith('variants ')) {
2318
+ // /offer variants [id] β€” show all variants
2319
+ const offerId = args.split(' ')[1];
2320
+ const result = salesEngine.getVariants(offerId);
2321
+ if (!result.success) {
2322
+ await apiCaller.sendTelegramMessage(chatId, `❌ ${result.error}`);
2323
+ } else {
2324
+ const lines = result.variants.map(v => {
2325
+ const convRate = v.clicks > 0 ? ((v.sales / v.clicks) * 100).toFixed(1) : '0.0';
2326
+ return `πŸ”€ <b>${v.id}:</b> ${v.title}\n πŸ“Š ${v.sales} sales | ${v.clicks} clicks | ${convRate}% conv | ${v.isActive ? '🟒 Active' : 'πŸ”΄ Paused'}`;
2327
+ }).join('\n\n');
2328
+ await apiCaller.sendTelegramMessage(chatId, `πŸ§ͺ <b>Variants: ${result.offerTitle || offerId}</b>\n\n${lines}`);
2329
+ }
2330
  } else if (args.toLowerCase().startsWith('eval')) {
2331
  // /offer eval β€” run evaluation
2332
  await apiCaller.sendTelegramMessage(chatId, 'πŸ“Š <i>Evaluating offers...</i>');
 
2379
  );
2380
  }
2381
 
2382
+ } else if (userText.toLowerCase().startsWith('/costs')) {
2383
+ const costs = costTracker.getCosts();
2384
+ let msg = `πŸ’Έ <b>API Cost Tracker</b>\n\n`;
2385
+ msg += `πŸ“Š <b>Total Spend:</b> $${(costs.total || 0).toFixed(4)}\n\n`;
2386
+ const models = Object.entries(costs.by_model || {}).sort((a, b) => b[1] - a[1]);
2387
+ if (models.length > 0) {
2388
+ msg += `<b>By Model:</b>\n`;
2389
+ for (const [model, cost] of models) {
2390
+ msg += `β€’ ${model}: $${cost.toFixed(4)}\n`;
2391
+ }
2392
+ } else {
2393
+ msg += `<i>No costs tracked yet.</i>`;
2394
+ }
2395
+ await apiCaller.sendTelegramMessage(chatId, msg);
2396
+
2397
+ } else if (userText.toLowerCase().startsWith('/scout')) {
2398
+ const keyword = userText.split(' ').slice(1).join(' ').trim();
2399
+ if (!keyword) {
2400
+ await apiCaller.sendTelegramMessage(chatId, `❌ Usage: <code>/scout [keyword]</code>\nExample: /scout AI productivity tools`);
2401
+ return;
2402
+ }
2403
+ await apiCaller.sendTelegramMessage(chatId, `πŸ” <b>Scouting:</b> "${keyword}"...\nAnalyzing competition, volume, and angles...`);
2404
+ const result = await scoutAgent.runScout(keyword);
2405
+ await apiCaller.sendTelegramMessage(chatId, `πŸ•΅οΈ <b>Scout Report: ${keyword}</b>\n\n${result}`);
2406
+
2407
+ } else if (userText.toLowerCase().startsWith('/landing')) {
2408
+ const publicUrl = process.env.PUBLIC_URL || `https://${process.env.SPACE_ID || 'aigoose-vinos-engine'}.hf.space`;
2409
+ await apiCaller.sendTelegramMessage(chatId, `πŸ”— <b>Your Landing Page:</b>\n${publicUrl}/landing\n\nShare this link as your link-in-bio!`);
2410
+
2411
  } else {
2412
  await handleVinIntent(chatId, from, userText);
2413
  }
skills/sales_engine.js CHANGED
@@ -233,53 +233,189 @@ module.exports = {
233
  return { success: true, transaction: txn, offer };
234
  },
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  async evaluateOffers() {
237
  const db = readSalesDB();
238
  const now = Date.now();
239
  let actions = [];
240
 
241
  for (const offer of db.offers) {
242
- if (offer.status !== 'active') continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
  const ageHours = (now - new Date(offer.created).getTime()) / 3600000;
245
  const salesLast24h = db.transactions.filter(t =>
246
  t.offerId === offer.id && (now - new Date(t.ts).getTime()) < 86400000
247
  ).length;
248
 
 
 
249
  if (salesLast24h >= 3) {
250
  // Winner! Scale it
251
  actions.push({ offerId: offer.id, action: 'scale', salesLast24h });
252
  await this.scaleWinner(offer.id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  } else if (ageHours >= 72 && offer.totalSales === 0) {
254
- // No sales in 72h β†’ pause
255
  offer.status = 'paused';
256
  actions.push({ offerId: offer.id, action: 'pause', reason: 'no_sales_72h' });
257
  }
258
-
259
- // Re-check pending_validation offers
260
- if (offer.status === 'pending_validation' && offer.nextCheck && new Date(offer.nextCheck) <= new Date()) {
261
- try {
262
- const trendValidator = require('./trend_validator');
263
- const recheck = await trendValidator.validateTopic(offer.topic);
264
- if (recheck.verdict === 'go') {
265
- // Promote to active offer creation
266
- offer.status = 'retired'; // retire the pending one
267
- await this.createOfferFromTopic(offer.topic, offer.priceIDR, offer.sourcePostIds[0], offer.sourcePillar);
268
- actions.push({ offerId: offer.id, action: 'promoted_from_wait' });
269
- } else if (recheck.verdict === 'skip') {
270
- offer.status = 'retired';
271
- actions.push({ offerId: offer.id, action: 'retired_after_recheck' });
272
- } else {
273
- offer.nextCheck = new Date(Date.now() + 48 * 3600000).toISOString();
274
- }
275
- } catch (e) { /* skip recheck on error */ }
276
- }
277
  }
278
 
279
  writeSalesDB(db);
280
 
 
 
 
 
 
 
281
  if (actions.length > 0) {
282
- const summary = actions.map(a => `β€’ ${a.offerId}: ${a.action}`).join('\n');
283
  await apiCaller.sendTelegramMessage(process.env.TELEGRAM_CHAT_ID,
284
  `πŸ“‹ <b>Offer Evaluation Complete</b>\n\n${summary}`
285
  );
@@ -389,12 +525,19 @@ module.exports = {
389
  };
390
  },
391
 
392
- injectOfferLink(topic) {
393
  const db = readSalesDB();
 
394
  const matching = db.offers
395
  .filter(o => o.status === 'active' && o.paymentUrl)
396
- .find(o => topic.toLowerCase().includes(o.topic.toLowerCase().split(' ')[0]) ||
397
- o.topic.toLowerCase().includes(topic.toLowerCase().split(' ')[0]));
398
- return matching ? { url: matching.paymentUrl, title: matching.title, priceIDR: matching.priceIDR } : null;
 
 
 
 
 
 
399
  }
400
  };
 
233
  return { success: true, transaction: txn, offer };
234
  },
235
 
236
+ async createVariant(offerId) {
237
+ const db = readSalesDB();
238
+ const offer = db.offers.find(o => o.id === offerId);
239
+ if (!offer) return { success: false, error: 'Offer not found' };
240
+
241
+ // Initialize variants array if not present (backward compat)
242
+ if (!offer.variants) {
243
+ offer.variants = [{
244
+ id: 'v0', title: offer.title, description: offer.description,
245
+ priceIDR: offer.priceIDR, benefits: offer.benefits || [],
246
+ impressions: 0, clicks: 0, sales: offer.totalSales || 0, isActive: true
247
+ }];
248
+ }
249
+
250
+ const existingStats = offer.variants.map(v =>
251
+ `Variant ${v.id}: "${v.title}" β€” ${v.sales} sales, ${v.clicks} clicks`
252
+ ).join('\n');
253
+
254
+ // Load failure patterns for context
255
+ const mainDb = memory.readDB();
256
+ const failureHints = (mainDb.failure_patterns || []).slice(0, 3).map(p => `- ${p}`).join('\n');
257
+
258
+ const prompt = [
259
+ { role: 'system', content: 'You are an A/B testing expert for digital products. Create a DIFFERENT angle/hook than existing variants. Output JSON only.' },
260
+ { role: 'user', content: `Topic: "${offer.topic}"\nPrice: Rp ${offer.priceIDR.toLocaleString()}\n\nExisting variants:\n${existingStats}\n\n${failureHints ? `Known failure patterns to avoid:\n${failureHints}\n\n` : ''}Create variant ${offer.variants.length} with a different angle, hook, or value proposition.\nJSON: { "title": string, "description": string, "benefits": [string, string, string] }` }
261
+ ];
262
+
263
+ const aiResult = await apiCaller.callOpenRouter(prompt);
264
+ let variantData = { title: `${offer.topic} (V${offer.variants.length})`, description: offer.description, benefits: [] };
265
+ if (aiResult.success) {
266
+ try {
267
+ const parsed = JSON.parse(aiResult.data.replace(/```json?\n?|```/g, '').trim());
268
+ variantData = { ...variantData, ...parsed };
269
+ } catch (e) { /* use default */ }
270
+ }
271
+
272
+ const variant = {
273
+ id: `v${offer.variants.length}`,
274
+ title: variantData.title,
275
+ description: variantData.description,
276
+ priceIDR: offer.priceIDR,
277
+ benefits: variantData.benefits || [],
278
+ impressions: 0, clicks: 0, sales: 0, isActive: true
279
+ };
280
+ offer.variants.push(variant);
281
+ writeSalesDB(db);
282
+
283
+ await apiCaller.sendTelegramMessage(process.env.TELEGRAM_CHAT_ID,
284
+ `πŸ§ͺ <b>New Variant Created!</b>\n\n` +
285
+ `πŸ“¦ Offer: ${offer.title}\n` +
286
+ `πŸ”€ Variant ${variant.id}: "${variant.title}"\n` +
287
+ `πŸ“ ${variant.description}\n\n` +
288
+ `Total variants: ${offer.variants.length}`
289
+ );
290
+
291
+ return { success: true, variant, offer };
292
+ },
293
+
294
+ getVariants(offerId) {
295
+ const db = readSalesDB();
296
+ const offer = db.offers.find(o => o.id === offerId);
297
+ if (!offer) return { success: false, error: 'Offer not found' };
298
+ if (!offer.variants || offer.variants.length === 0) {
299
+ return { success: true, variants: [{ id: 'v0', title: offer.title, sales: offer.totalSales, clicks: 0, impressions: 0, isActive: true }] };
300
+ }
301
+ return { success: true, variants: offer.variants, offerTitle: offer.title };
302
+ },
303
+
304
+ trackClick(offerId, variantId) {
305
+ const db = readSalesDB();
306
+ const offer = db.offers.find(o => o.id === offerId);
307
+ if (!offer) return null;
308
+ if (offer.variants) {
309
+ const variant = offer.variants.find(v => v.id === variantId);
310
+ if (variant) { variant.clicks = (variant.clicks || 0) + 1; variant.impressions = (variant.impressions || 0) + 1; }
311
+ }
312
+ writeSalesDB(db);
313
+ return offer.paymentUrl;
314
+ },
315
+
316
+ async analyzeFailurePatterns() {
317
+ const db = readSalesDB();
318
+ const retired = db.offers.filter(o => o.status === 'retired' || o.status === 'paused');
319
+ if (retired.length < 3) return { success: false, reason: 'Need at least 3 retired/paused offers to analyze' };
320
+
321
+ const retiredSummary = retired.map(o =>
322
+ `- "${o.title}" (${o.topic}): ${o.totalSales} sales, Rp ${o.priceIDR}, pillar: ${o.sourcePillar || 'general'}, trend: ${o.trendScore || 'N/A'}`
323
+ ).join('\n');
324
+
325
+ const prompt = [
326
+ { role: 'system', content: 'You are a sales strategist analyzing failed digital product offers. Extract 3-5 actionable patterns. Be specific about what to avoid. Output as a JSON array of strings.' },
327
+ { role: 'user', content: `Retired/paused offers:\n${retiredSummary}\n\nExtract failure patterns as JSON array: ["pattern 1", "pattern 2", ...]` }
328
+ ];
329
+
330
+ const aiResult = await apiCaller.callOpenRouter(prompt);
331
+ let patterns = [];
332
+ if (aiResult.success) {
333
+ try {
334
+ patterns = JSON.parse(aiResult.data.replace(/```json?\n?|```/g, '').trim());
335
+ } catch (e) { patterns = [aiResult.data.substring(0, 200)]; }
336
+ }
337
+
338
+ const mainDb = memory.readDB();
339
+ mainDb.failure_patterns = patterns;
340
+ memory.writeDB(mainDb);
341
+
342
+ return { success: true, patterns };
343
+ },
344
+
345
  async evaluateOffers() {
346
  const db = readSalesDB();
347
  const now = Date.now();
348
  let actions = [];
349
 
350
  for (const offer of db.offers) {
351
+ if (offer.status !== 'active') {
352
+ // Re-check pending_validation offers
353
+ if (offer.status === 'pending_validation' && offer.nextCheck && new Date(offer.nextCheck) <= new Date()) {
354
+ try {
355
+ const trendValidator = require('./trend_validator');
356
+ const recheck = await trendValidator.validateTopic(offer.topic);
357
+ if (recheck.verdict === 'go') {
358
+ offer.status = 'retired';
359
+ await this.createOfferFromTopic(offer.topic, offer.priceIDR, offer.sourcePostIds[0], offer.sourcePillar);
360
+ actions.push({ offerId: offer.id, action: 'promoted_from_wait' });
361
+ } else if (recheck.verdict === 'skip') {
362
+ offer.status = 'retired';
363
+ actions.push({ offerId: offer.id, action: 'retired_after_recheck' });
364
+ } else {
365
+ offer.nextCheck = new Date(Date.now() + 48 * 3600000).toISOString();
366
+ }
367
+ } catch (e) { /* skip recheck on error */ }
368
+ }
369
+ continue;
370
+ }
371
 
372
  const ageHours = (now - new Date(offer.created).getTime()) / 3600000;
373
  const salesLast24h = db.transactions.filter(t =>
374
  t.offerId === offer.id && (now - new Date(t.ts).getTime()) < 86400000
375
  ).length;
376
 
377
+ const variantCount = (offer.variants || []).length;
378
+
379
  if (salesLast24h >= 3) {
380
  // Winner! Scale it
381
  actions.push({ offerId: offer.id, action: 'scale', salesLast24h });
382
  await this.scaleWinner(offer.id);
383
+
384
+ // If variants exist, pause losers
385
+ if (offer.variants && offer.variants.length > 1) {
386
+ const sorted = [...offer.variants].sort((a, b) => (b.sales || 0) - (a.sales || 0));
387
+ for (let i = 1; i < sorted.length; i++) {
388
+ sorted[i].isActive = false;
389
+ }
390
+ actions.push({ offerId: offer.id, action: 'variant_loser_paused', winner: sorted[0].id });
391
+ }
392
+
393
+ } else if (ageHours >= 48 && offer.totalSales === 0 && variantCount < 2) {
394
+ // Stale 48h, no sales, < 2 variants β†’ auto-create variant instead of waiting
395
+ try {
396
+ await this.createVariant(offer.id);
397
+ actions.push({ offerId: offer.id, action: 'auto_variant_created', reason: 'no_sales_48h' });
398
+ } catch (e) {
399
+ actions.push({ offerId: offer.id, action: 'variant_create_failed', error: e.message });
400
+ }
401
+
402
  } else if (ageHours >= 72 && offer.totalSales === 0) {
403
+ // 72h, still 0 sales even with variants β†’ pause
404
  offer.status = 'paused';
405
  actions.push({ offerId: offer.id, action: 'pause', reason: 'no_sales_72h' });
406
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  }
408
 
409
  writeSalesDB(db);
410
 
411
+ // Analyze failure patterns if enough retired offers
412
+ const retiredCount = db.offers.filter(o => o.status === 'retired' || o.status === 'paused').length;
413
+ if (retiredCount >= 3) {
414
+ try { await this.analyzeFailurePatterns(); } catch (e) { /* non-critical */ }
415
+ }
416
+
417
  if (actions.length > 0) {
418
+ const summary = actions.map(a => `β€’ ${a.offerId}: ${a.action}${a.reason ? ` (${a.reason})` : ''}`).join('\n');
419
  await apiCaller.sendTelegramMessage(process.env.TELEGRAM_CHAT_ID,
420
  `πŸ“‹ <b>Offer Evaluation Complete</b>\n\n${summary}`
421
  );
 
525
  };
526
  },
527
 
528
+ injectOfferLink(topic, source = 'social') {
529
  const db = readSalesDB();
530
+ const topicLower = topic.toLowerCase();
531
  const matching = db.offers
532
  .filter(o => o.status === 'active' && o.paymentUrl)
533
+ .find(o => {
534
+ const offerWords = o.topic.toLowerCase().split(/\s+/);
535
+ const topicWords = topicLower.split(/\s+/);
536
+ return offerWords.some(w => w.length > 3 && topicLower.includes(w)) ||
537
+ topicWords.some(w => w.length > 3 && o.topic.toLowerCase().includes(w));
538
+ });
539
+ if (!matching) return null;
540
+ const utm = `?utm_source=${source}&utm_medium=${source === 'seo' ? 'article' : 'post'}&utm_campaign=${encodeURIComponent(topic.replace(/\s+/g, '-').toLowerCase())}`;
541
+ return { url: matching.paymentUrl, utmUrl: matching.paymentUrl + utm, title: matching.title, priceIDR: matching.priceIDR, offerId: matching.id };
542
  }
543
  };
skills/set_telegram_menu.js CHANGED
@@ -36,7 +36,11 @@ const setCommands = async () => {
36
  { command: 'pulseconfig', description: 'βš™οΈ Configure Daily Pulse (v2.0)' },
37
  { command: 'viral', description: '🦠 Viral Content Engine (v1.0)' },
38
  { command: 'offer', description: 'πŸ’° Sales Offers (create/list/pause)' },
39
- { command: 'revenue', description: 'πŸ“Š Revenue Dashboard & Summary' }
 
 
 
 
40
  ];
41
 
42
  try {
 
36
  { command: 'pulseconfig', description: 'βš™οΈ Configure Daily Pulse (v2.0)' },
37
  { command: 'viral', description: '🦠 Viral Content Engine (v1.0)' },
38
  { command: 'offer', description: 'πŸ’° Sales Offers (create/list/pause)' },
39
+ { command: 'revenue', description: 'πŸ“Š Revenue Dashboard & Summary' },
40
+ { command: 'costs', description: 'πŸ’Έ API Spend Breakdown' },
41
+ { command: 'scout', description: 'πŸ” Market Viability Scout' },
42
+ { command: 'seo2offer', description: '🎯 Full SEO-to-Revenue Pipeline' },
43
+ { command: 'landing', description: 'πŸ”— Link-in-Bio Page' }
44
  ];
45
 
46
  try {
skills/wordpress_publisher.js CHANGED
@@ -66,7 +66,25 @@ class WordPressPublisher {
66
  // Also handle any leftover Pollinations <img> tags (from older articles)
67
  htmlWithMedia = htmlWithMedia.replace(/<img[^>]+src="https:\/\/image\.pollinations\.ai[^"]*"[^>]*>/g, '');
68
 
69
- const finalContent = `${htmlWithMedia}\n\n<!-- SEO Schema (Generated by VinOS) -->\n${articleData.schema}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  // Handle Tags
72
  let tagIds = [];
 
66
  // Also handle any leftover Pollinations <img> tags (from older articles)
67
  htmlWithMedia = htmlWithMedia.replace(/<img[^>]+src="https:\/\/image\.pollinations\.ai[^"]*"[^>]*>/g, '');
68
 
69
+ // CTA injection β€” match active sales offer to article keyword
70
+ let ctaBlock = '';
71
+ try {
72
+ const salesEngine = require('./sales_engine');
73
+ const keyword = articleData.meta?.keyword || articleData.meta?.title || '';
74
+ const offerMatch = salesEngine.injectOfferLink(keyword, 'seo');
75
+ if (offerMatch) {
76
+ ctaBlock = `\n<div class="vinos-cta" style="margin:32px 0;padding:24px;background:#f8f9fa;border-left:4px solid #22d3a0;border-radius:8px;">` +
77
+ `<h3 style="margin:0 0 8px;font-size:1.2rem;">${offerMatch.title}</h3>` +
78
+ `<p style="margin:0 0 12px;color:#666;">Rp ${(offerMatch.priceIDR || 0).toLocaleString('id-ID')}</p>` +
79
+ `<a href="${offerMatch.utmUrl}" target="_blank" rel="noopener" style="display:inline-block;padding:10px 24px;background:#22d3a0;color:#fff;text-decoration:none;border-radius:6px;font-weight:bold;">Get It Now &rarr;</a>` +
80
+ `</div>`;
81
+ console.log(`[WP Publisher] CTA injected for offer: ${offerMatch.title}`);
82
+ }
83
+ } catch (e) {
84
+ console.error('[WP Publisher] CTA injection skipped:', e.message);
85
+ }
86
+
87
+ const finalContent = `${htmlWithMedia}${ctaBlock}\n\n<!-- SEO Schema (Generated by VinOS) -->\n${articleData.schema}`;
88
 
89
  // Handle Tags
90
  let tagIds = [];