Promo toolkit by badge + AI Copy Engine at Surge: posts, DMs, follow-ups, objection replies, emails, stories in the member's name; monthly free allowance by badge (20/60/150), then ad credits per generation; admin usage view

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-14 06:42:55 -05:00
parent b09e5dce8b
commit c1f58b2acf
8 changed files with 212 additions and 5 deletions
BIN
View File
Binary file not shown.
+1
View File
@@ -82,6 +82,7 @@ FACTS:
- DAILY CLAIM STREAK: finishing the daily ad set and claiming pays 5 credits on day 1, 7 on day 2, 10 from day 3, and 25 on every 7th consecutive day; miss a day and it restarts. After the set, verified visits (up to 20 a day, 1 credit each) keep earning, and the credits are meant to be spent on a campaign.
- BLOG (public, instantadpay.com/blog): Marty's coaching and teaching articles on building a line, advertising that pays, and daily habits; each article has its own page and can be shared; RSS at /blog/feed.xml. Members who want to write their own articles: not offered today.
- ACHIEVEMENT BADGES ON TELEGRAM (2026-09-13): when a member unlocks Spark/Surge/Circuit/Nexus, their personalised badge image (username on the ribbon) is posted automatically to the team's Telegram payments topic and the main group, once per badge; members cannot trigger posts themselves (the 'Post to Telegram' button is admin-only); 'Share' opens a picker (X, Facebook, Telegram, WhatsApp, LinkedIn, Massifly (copies the post and opens the feed composer), the phone's own share menu, copy link, save image) that shares the member's public badge page instantadpay.com/b/<username>/<badge>, which shows the badge and their join link.
- PROMO TOOLKIT BY BADGE + AI COPY ENGINE (2026-09-14): Promo tools > AI Copy Engine shows the ladder: Free (links, posts, swipes, banners, wall, objections, shorts, badge pages), Spark (payouts on; campaign templates and QR handout coming), Surge (first qualifying buyer; the AI Copy Engine is live: posts, DMs, follow-ups, objection replies, emails, story posts in the member's name with their link, honesty rules built in; 20 free generations a month), Circuit (60 free; video end cards and split tester coming), Nexus (150 free; leader ops, co-branded pages, team grants, own partner kit coming). After the free allowance each generation costs 10 ad credits from the earned pool. Credits are advertising, never money.
- GETTING STARTED CARD (2026-09-14): the top of every new member's Overview shows four steps (username, link wallet, switch on payouts, first package) with the current one explained and one button that opens the right tab and highlights the control (Wallet tab > Link wallet; then Activate payouts). Encouragement only: it can be hidden, and members who only want to view ads and earn credits are never blocked. It disappears once all four are done.
- LEADERBOARD + REFERRAL CONTEST (2026-09-14): instantadpay.com/leaderboard ranks members by ad packages sold to people they directly sponsor (dollar value, read from the chain; a member's own linked positions and second accounts never count). Periods: this week (Monday to Sunday, Central time), this month, all time. Weekly and monthly winners are recorded automatically at rollover, announced in Telegram, and the credit prizes set by the admin (a ladder: 1st 1,000 / 2nd 500 / 3rd 250 weekly, 5,000 / 2,500 / 1,000 monthly by default) are granted automatically to those positions; the prize text is shown on the page and on the Overview's Leaderboard card, which also shows the member's own rank. Prizes are credits or packages, never cash.
- WHAT'S NEW / ROADMAP (2026-09-14): instantadpay.com/whats-new lists release notes (what shipped, dated, tagged new/improved/fixed) and the roadmap (planned / building, with rough ETAs). The Overview has a "What's new" card with the latest three notes and what is being built; a dot marks notes since the member's last look. Written by the admin in Admin > Releases.
+1 -1
View File
@@ -757,7 +757,7 @@
}));
// site settings: key / value rows; booleans as checkboxes, numbers stay numbers
const SITE_META = { siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', memberWeeklyEmail: 'Weekly member email to everyone active (1) or only sponsors with a line (0)', leaderboardWeeklyPrize: 'Leaderboard: weekly prize text (optional; blank shows the credit ladder)', leaderboardMonthlyPrize: 'Leaderboard: monthly prize text (optional)', leaderboardWeeklyCredits: 'Leaderboard: weekly credits for 1st,2nd,3rd… (e.g. 1000,500,250; blank = none)', leaderboardMonthlyCredits: 'Leaderboard: monthly credits for 1st,2nd,3rd… (e.g. 5000,2500,1000)', leaderboardAnnounceGeneral: 'Leaderboard: announce winners in the main group too (1/0)', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', legacyCreditsAdvertiser: 'Legacy welcome credits: former advertisers', legacyCreditsEarner: 'Legacy welcome credits: former earners', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' };
const SITE_META = { siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', aiCreditsPerGen: 'AI Copy Engine: credits per generation after the free allowance', aiFreeSurge: 'AI Copy Engine: free generations a month at Surge', aiFreeCircuit: 'AI Copy Engine: free generations a month at Circuit', aiFreeNexus: 'AI Copy Engine: free generations a month at Nexus', memberWeeklyEmail: 'Weekly member email to everyone active (1) or only sponsors with a line (0)', leaderboardWeeklyPrize: 'Leaderboard: weekly prize text (optional; blank shows the credit ladder)', leaderboardMonthlyPrize: 'Leaderboard: monthly prize text (optional)', leaderboardWeeklyCredits: 'Leaderboard: weekly credits for 1st,2nd,3rd… (e.g. 1000,500,250; blank = none)', leaderboardMonthlyCredits: 'Leaderboard: monthly credits for 1st,2nd,3rd… (e.g. 5000,2500,1000)', leaderboardAnnounceGeneral: 'Leaderboard: announce winners in the main group too (1/0)', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', legacyCreditsAdvertiser: 'Legacy welcome credits: former advertisers', legacyCreditsEarner: 'Legacy welcome credits: former earners', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' };
function drawSite() {
const wrap = $('siteForm');
wrap.innerHTML = Object.entries(siteObj).map(([k, v]) => '<div class="kv-row"><span class="k" title="' + esc(k) + '">' + esc(SITE_META[k] || humanize(k)) + '</span>'
+31 -1
View File
@@ -1473,9 +1473,39 @@
}
document.querySelectorAll('.subtabs [data-earn]').forEach(b =>
b.addEventListener('click', () => setEarnSub(b.dataset.earn)));
// promo toolkit: badge tiers + the AI Copy Engine (Marty, 2026-09-14)
let tkState = null;
async function loadToolkit() {
try {
const r = await (await fetch('/api/my/toolkit')).json(); if (r.error) return; tkState = r;
const esc = t => String(t == null ? '' : t).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const names = { free: 'Free', spark: 'Spark', surge: 'Surge', circuit: 'Circuit', nexus: 'Nexus' };
$('tkTierLine').textContent = 'you are at ' + names[r.tier];
$('tkTiers').innerHTML = r.tiers.map(t => '<div class="tk-tier' + (t.reached ? ' reached' : '') + (t.key === r.tier ? ' current' : '') + '"><b>' + esc(t.name) + (t.reached ? ' ✓' : '') + '</b><div class="need">' + (t.reached ? esc(t.blurb) : 'Unlocks when you ' + esc(t.needText)) + '</div><ul>' + t.items.map(i => '<li' + (i.live ? '' : ' class="soon"') + '>' + esc(i.t) + '</li>').join('') + '</ul>' + (t.reached ? '' : '<div class="lock" style="margin-top:8px">locked</div>') + '</div>').join('');
$('tkLocked').hidden = r.unlocked; $('tkForm').hidden = !r.unlocked;
if (!r.unlocked) { $('tkLocked').textContent = 'Unlocks at Surge: your first qualifying buyer of a $20 or larger package. Then it writes posts, DMs, follow-ups, objection replies, emails and story posts in your name, with your link, inside the honesty rules. ' + (r.tier === 'free' ? 'Step one is switching on payouts.' : 'You are one buyer away.'); $('tkUsage').textContent = 'locked'; return; }
$('tkUsage').textContent = r.freeLeft + ' free this month · then ' + r.cost + ' credits each · you have ' + r.available.toLocaleString() + ' credits';
$('tkCostLine').textContent = r.freeLeft > 0 ? 'This one is free (' + r.freeLeft + ' left this month).' : 'This one costs ' + r.cost + ' credits from your earned pool.';
if (!$('tkKind').options.length) { $('tkKind').innerHTML = r.kinds.map(k => '<option value="' + k.key + '">' + esc(k.label) + '</option>').join(''); $('tkAngle').innerHTML = r.angles.map(a => '<option value="' + a + '">' + a + '</option>').join(''); }
$('tkHistory').innerHTML = r.history.length ? '<p class="small muted" style="margin:0 0 4px">Recent</p>' + r.history.map(h => '<div class="tk-hist"><span class="muted">' + esc(h.kind) + ' · ' + new Date(h.ts).toLocaleString() + (h.charged ? ' · ' + h.charged + ' credits' : ' · free') + '</span><br>' + esc(h.text).replace(/\n/g, '<br>') + '</div>').join('') : '';
} catch (e) {}
}
async function tkGenerate() {
const btn = $('tkGo'); if (btn.disabled) return; btn.disabled = true; btn.textContent = 'Writing…';
try {
const r = await (await fetch('/api/my/toolkit/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: $('tkKind').value, angle: $('tkAngle').value, brief: $('tkBrief').value }) })).json();
if (r.error) { IAP.status(r.error, 'bad'); return; }
$('tkText').value = r.text; $('tkOut').hidden = false;
IAP.status(r.charged ? r.charged + ' credits used.' : 'Written. ' + r.freeLeft + ' free left this month.', 'ok');
loadToolkit(); loadDashboard();
} catch (e) { IAP.status('The engine did not answer. Try again.', 'bad'); }
finally { btn.disabled = false; btn.textContent = 'Write it'; }
}
if ($('tkGo')) { $('tkGo').addEventListener('click', tkGenerate); $('tkAgain').addEventListener('click', tkGenerate); $('tkCopy').addEventListener('click', async () => { try { await navigator.clipboard.writeText($('tkText').value); IAP.status('Copied.', 'ok'); } catch (e) { $('tkText').select(); } }); }
// promo tools: pill menu switches between posts / swipes / banners / wall / videos
function setPromoSub(name) {
const ids = ['posts', 'text', 'swipe', 'banners', 'wall', 'objections', 'videos'];
const ids = ['toolkit', 'posts', 'text', 'swipe', 'banners', 'wall', 'objections', 'videos'];
if (name === 'toolkit') loadToolkit();
if (!ids.includes(name)) name = 'posts';
ids.forEach(id => { const el = $('promo-' + id); if (el) el.hidden = id !== name; });
document.querySelectorAll('.promo-pills [data-promo]').forEach(b => {
+9
View File
@@ -690,3 +690,12 @@ img{max-width:100%}
.gs-now p{margin:0;flex:1;min-width:240px;max-width:60ch} .gs-now p b{display:block;font-family:var(--disp);font-size:16px;margin-bottom:2px}
@keyframes gsPulse{0%{box-shadow:0 0 0 0 rgba(67,232,195,.7)}70%{box-shadow:0 0 0 14px rgba(67,232,195,0)}100%{box-shadow:0 0 0 0 rgba(67,232,195,0)}}
.pulse{animation:gsPulse 1.2s ease-out 4}
/* promo toolkit tiers */
.tk-tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px}
.tk-tier{border:1px solid var(--line);border-radius:12px;padding:12px 14px;font-size:13.5px;color:var(--muted)}
.tk-tier.reached{border-color:rgba(67,232,195,.5);color:var(--ink)} .tk-tier.current{background:rgba(67,232,195,.07)}
.tk-tier b{display:block;font-family:var(--disp);font-size:15px;color:var(--ink)} .tk-tier .need{font-size:12px;margin:2px 0 8px;color:var(--muted)}
.tk-tier ul{margin:0;padding-left:16px} .tk-tier li{margin:3px 0} .tk-tier li.soon{opacity:.65} .tk-tier li.soon::after{content:' (coming)';font-size:11px;color:var(--muted)}
.tk-tier .lock{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#ffd15c}
.tk-hist{border-top:1px solid var(--line);padding:8px 0;font-size:13px} .tk-hist .muted{font-size:12px}
+23 -2
View File
@@ -5,7 +5,7 @@
<title>Member area | InstantAdPay</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="icon" type="image/png" href="/logo-icon.png">
<link rel="stylesheet" href="/assets/site.css?v=20260914a">
<link rel="stylesheet" href="/assets/site.css?v=20260914b">
</head>
<body class="bo-body">
@@ -679,6 +679,7 @@
<span><button class="btn small sec" id="promoWallCopy" type="button">Copy wall link</button> <a class="btn small sec" id="promoWallOpen" href="#" target="_blank" rel="noopener">Open</a></span>
</div>
<div class="chips promo-pills" role="tablist" aria-label="Promo tools sections">
<button class="chip-t" data-promo="toolkit" type="button" role="tab">✨ AI Copy Engine</button>
<button class="chip-t on" data-promo="posts" type="button" role="tab">Social posts</button>
<button class="chip-t" data-promo="text" type="button" role="tab">Text a friend</button>
<button class="chip-t" data-promo="swipe" type="button" role="tab">Email swipes</button>
@@ -687,6 +688,26 @@
<button class="chip-t" data-promo="objections" type="button" role="tab">Objections</button>
<button class="chip-t" data-promo="videos" type="button" role="tab">Videos</button>
</div>
<div class="card promo-sec" id="promo-toolkit" hidden>
<div class="card-head"><h3>Your toolkit</h3><span class="sub" id="tkTierLine"></span></div>
<p class="muted small" style="margin:0 0 10px">Every badge unlocks more. Free tools are yours from day one; the AI Copy Engine opens at Surge, your first qualifying buyer.</p>
<div class="tk-tiers" id="tkTiers"></div>
<div id="tkEngine" style="margin-top:16px">
<div class="card-head"><h3>AI Copy Engine</h3><span class="sub" id="tkUsage"></span></div>
<div id="tkLocked" class="muted small" hidden></div>
<div id="tkForm" hidden>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin:0 0 8px">
<label class="small">What to write<select id="tkKind" style="width:100%"></select></label>
<label class="small">Angle<select id="tkAngle" style="width:100%"></select></label>
</div>
<label class="small">Brief (who it is for, what they said, what you want to say; or leave it blank)<textarea id="tkBrief" rows="3" style="width:100%" maxlength="800" placeholder="e.g. a friend who runs a safelist and says he never gets paid on time"></textarea></label>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:8px 0"><button type="button" class="btn small" id="tkGo">Write it</button><span class="muted small" id="tkCostLine"></span></div>
<div id="tkOut" hidden><textarea id="tkText" rows="8" style="width:100%"></textarea>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin:8px 0 0"><button type="button" class="btn small sec" id="tkCopy">Copy</button><button type="button" class="btn small sec" id="tkAgain">Another take</button></div></div>
<div id="tkHistory" style="margin-top:14px"></div>
</div>
</div>
</div>
<div class="card promo-sec" id="promo-posts">
<h3>Social posts, personalized with your link</h3>
<p class="muted small">Copy and paste, or post straight from here. Every post already carries your invite link,
@@ -924,7 +945,7 @@
<script src="/assets/common.js?v=20260914a"></script>
<script src="/assets/wallet.js?v=20260911a"></script>
<script src="/assets/promo.js?v=20260911a"></script>
<script src="/assets/my.js?v=20260914c"></script>
<script src="/assets/my.js?v=20260914d"></script>
<script src="/assets/chat.js?v=20260907l"></script>
</body>
</html>
+21 -1
View File
@@ -32,7 +32,8 @@ const blog = require('./blog');
const adminMember = require('./adminmember');
const syndicate = require('./syndicate');
const releases = require('./releases');
const leaderboard = require('./leaderboard'); // referral contest: /leaderboard, Overview card, weekly + monthly winners (Marty, 2026-09-14) // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
const leaderboard = require('./leaderboard');
const toolkit = require('./toolkit'); // badge-gated promo toolkit + AI Copy Engine (Surge and up) (Marty, 2026-09-14) // referral contest: /leaderboard, Overview card, weekly + monthly winners (Marty, 2026-09-14) // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog', '/whats-new', '/leaderboard']);
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
@@ -368,6 +369,7 @@ async function boot() {
loadOpenTokens();
syndicate.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR, uploadsDir: UPLOADS_DIR });
releases.init({ dataDir: DATA_DIR });
toolkit.init({ dataDir: DATA_DIR, ads, accounts, siteConfig });
leaderboard.init({ chain, accounts, ads, dataDir: DATA_DIR, siteConfig, pushFeed, adminEmail: ADMIN_EMAIL,
notify: async text => { const sc = siteConfig(); if (!sc.telegramBotToken || !sc.telegramEchoChatId) return; await telegramSend(sc.telegramEchoChatId, text, sc.telegramEchoTopicId); if (String(sc.leaderboardAnnounceGeneral || '1') !== '0') await telegramSend(sc.telegramEchoChatId, text, null); } });
setTimeout(() => leaderboard.rolloverTick().catch(e => console.error('leaderboard rollover', e.message)), 90 * 1000);
@@ -405,6 +407,7 @@ function siteConfig() {
telegramEchoChatId: '', telegramEchoTopicId: '', telegramEchoEvents: 'payouts', // shared cross-program payments topic
telegramAdminChatId: '', // private chat for admin alerts (sign-up guard bursts); falls back to ADMIN_EMAIL
launchAt: '', // public launch moment, ISO 8601 with offset (e.g. 2026-09-18T19:00:00-05:00): countdown on /launch + dashboard mark
aiCreditsPerGen: 10, aiFreeSurge: 20, aiFreeCircuit: 60, aiFreeNexus: 150, // AI Copy Engine: free generations per month by badge, then credits per generation
memberWeeklyEmail: '0', // 1 = the weekly 'Your week on InstantAdPay' email goes to every active member, not only sponsors with a line
leaderboardWeeklyPrize: '', leaderboardMonthlyPrize: '', leaderboardWeeklyCredits: '1000,500,250', leaderboardMonthlyCredits: '5000,2500,1000', leaderboardAnnounceGeneral: '1', // referral contest prizes (text shown on /leaderboard; credits granted to the winner automatically at rollover)
legacyCreditsAdvertiser: 500, legacyCreditsEarner: 150, // welcome-back credits for listed Faucet Wave / Tier One Ads emails arriving via /from/<brand>
@@ -1222,6 +1225,23 @@ const server = http.createServer(async (req, res) => {
}
// -- release notes + roadmap (public read; admin write) (Marty, 2026-09-14)
if (p === '/api/releases' && req.method === 'GET') return json(res, 200, releases.publicView());
// -- promo toolkit: tiers by badge + the AI Copy Engine (Surge+), credits beyond the free allowance
if (p === '/api/my/toolkit' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await toolkit.status(s.email));
}
if (p === '/api/my/toolkit/generate' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await toolkit.generate(s.email, String(b.kind || ''), String(b.brief || ''), String(b.angle || 'plain'));
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/toolkit' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await toolkit.adminUsage());
}
if (p === '/api/leaderboard' && req.method === 'GET') { // public standings; signed-in members also get their own row
const s = await auth.fromRequest(req);
const period = ['week', 'month', 'all', 'lastweek', 'lastmonth'].includes(u.searchParams.get('period')) ? u.searchParams.get('period') : 'week';
+126
View File
@@ -0,0 +1,126 @@
// Badge-gated promo toolkit + the AI Copy Engine (Marty, 2026-09-14).
// Tiers follow the achievement badges: free (no badge), Spark (payouts on), Surge (first qualifying buyer),
// Circuit (two), Nexus (five). The first AI tool unlocks at Surge. Each generation uses a monthly free
// allowance by tier, then costs ad credits from the member's earned pool (credits are advertising, 1 = 1 cent).
// The engine is the same OpenRouter model the chatbot uses; the InstantAdPay facts, the compliance rules and
// the voice live here in version control and travel with every request.
const fs = require('fs');
const path = require('path');
const https = require('https');
let R = null; // { dataDir, ads, accounts, siteConfig }
const MODEL = process.env.OPENROUTER_MODEL || 'deepseek/deepseek-v4-flash:nitro';
function init(refs) { R = refs; }
function key() { if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY.trim(); try { return fs.readFileSync(path.join(R.dataDir, 'openrouter.key'), 'utf8').trim(); } catch (e) { return ''; } }
const FILE = () => path.join(R.dataDir, 'toolkit-usage.json');
function usage() { try { return JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) { return {}; } }
function saveUsage(u) { try { fs.writeFileSync(FILE(), JSON.stringify(u)); } catch (e) {} }
const monthKey = () => new Date().toLocaleDateString('en-US', { timeZone: 'America/Chicago', year: 'numeric', month: '2-digit' });
// ---- the ladder (what each badge unlocks; 'live' items exist today, the rest are on the roadmap) ----
const TIERS = [
{ key: 'free', name: 'Free', need: null, blurb: 'Everything you need to start sharing.', items: [
{ t: 'Invite link with six angle front doors', live: true }, { t: 'Social posts, text-a-friend, email swipes, banners', live: true }, { t: 'Banner wall and line banner', live: true }, { t: 'Objection handling and the shorts', live: true }, { t: 'Badge share pages', live: true }] },
{ key: 'spark', name: 'Spark', need: 'payouts', needText: 'switch on payouts', blurb: 'Your wallet is registered; the site can pay you.', items: [
{ t: 'Campaign templates: one-tap banner and text campaigns aimed at your link', live: false }, { t: 'Printable handout with your QR', live: false }] },
{ key: 'surge', name: 'Surge', need: 'firstBuyer', needText: 'your first qualifying buyer ($20+)', blurb: 'The first AI tool.', items: [
{ t: 'AI Copy Engine: posts, DMs, follow-ups, objection replies, emails and stories in the InstantAdPay voice', live: true }, { t: 'Monthly free generations, then ad credits', live: true }] },
{ key: 'circuit', name: 'Circuit', need: 'level2', needText: 'two qualifying buyers', blurb: 'Creative and traffic.', items: [
{ t: 'Three times the free AI generations', live: true }, { t: 'Video Maker: the shorts and the explainer with your own end card and QR', live: false }, { t: 'Split tester for your join angles', live: false }] },
{ key: 'nexus', name: 'Nexus', need: 'level3', needText: 'five qualifying buyers', blurb: 'Leader tools.', items: [
{ t: 'Seven times the free AI generations', live: true }, { t: 'Leader Ops: team triage, one-click nudges, AI-drafted team broadcasts', live: false }, { t: 'Co-branded pages and team credit grants', live: false }, { t: 'Your own partner kit and promo code', live: false }] }
];
function tierFor(badges) {
const has = k => badges.includes(k);
return has('level3') ? 'nexus' : has('level2') ? 'circuit' : has('firstBuyer') ? 'surge' : has('payouts') ? 'spark' : 'free';
}
function allowanceFor(tier, sc) {
const n = k => Number(sc[k]);
return tier === 'nexus' ? (n('aiFreeNexus') || 150) : tier === 'circuit' ? (n('aiFreeCircuit') || 60) : tier === 'surge' ? (n('aiFreeSurge') || 20) : 0;
}
// ---- the engine's standing orders ----
const COMPLIANCE = [
'NEVER promise, guarantee, project or imply income, earnings, returns or profit.',
'NEVER use hype: no "guaranteed", "risk-free", "passive income", "get rich", "financial freedom".',
'InstantAdPay SELLS ADVERTISING with a performance referral program. It is not an investment. Say so plainly when the length allows.',
'The only money facts you may state: packages are $5, $20, $50, $100 and $250 of ad credits (1 credit = 1 cent of ad delivery); when someone in your line buys a package the contract pays the direct sponsor 50% in the same transaction, 20% to level 2, 10% to level 3, 20% to the platform; two qualifying $20+ buyers open level 2, five open level 3. Never invent other numbers or member results.',
'Payments are POL on Polygon, wallet to wallet, on a public ledger anyone can check. Cryptocurrency carries real risk; results depend on effort.',
'No em dashes and no double hyphens anywhere. No fake urgency, no fake scarcity, no invented testimonials.'
].join(' ');
const FACTS = 'InstantAdPay (instantadpay.com) is an advertising platform: members join free by email, earn ad credits daily by viewing a short set of ads (the claim grows with a streak), and spend credits on their own banner, text, login, solo, video, featured and verified-visit campaigns across the site and a partner network; views are timed on the server so a real person saw the ad. Ad packages from $5 buy more reach. Every package sold pays the sponsor instantly on-chain in the same transaction; there is no balance to withdraw because nothing is held. The daily routine members are taught: do the five-ad set and spend the claim, message one person in your line, check the holding tank, have one conversation with someone who already pays for traffic. Two qualifying buyers open the next level; five open the one after. Founder: Marty Bostick; brought to you by the Crypto Team Build Network.';
const VOICE = 'Write like a straight-talking internet marketer who has been around long enough to hate hype: plain words, short sentences, honest about risk, warm and confident, never salesy. First person, as the member. Specific beats clever.';
const KINDS = {
post: { label: 'Social post', shape: 'Write ONE social media post of 40-90 words. Strong first line. No hashtags. At most one emoji. End with a soft invitation to look, not a hard sell, and put the link on its own last line.' },
dm: { label: 'Direct message', shape: 'Write ONE short direct message to a friend or contact, 30-60 words. Conversational, personal, zero pressure, sounds like a real text. Link on the last line.' },
followup: { label: 'Follow-up', shape: 'Write ONE follow-up message (30-70 words) to someone who looked but has not joined or bought. No guilt, no pressure, no fake urgency. Give them one useful reason to look again. Link on the last line.' },
objection: { label: 'Objection reply', shape: 'The brief is what the prospect said. Write ONE reply of 60-120 words: concede what is true first, give the honest answer, then invite them to check the ledger themselves. No link unless it helps.' },
email: { label: 'Email', shape: 'Write ONE short email: first line "Subject: ..." then a blank line, then a 90-160 word plain-text body ending with a sign-off and the link on its own line.' },
story: { label: 'Story post', shape: 'Write ONE first-person post of 80-140 words telling a small, believable personal story from the brief about advertising, building a line, or the daily routine. No invented numbers. Link on the last line.' }
};
const ANGLES = {
plain: 'General: advertising that pays the sponsor instantly, on-chain.',
advertisers: 'Angle: for people who already pay for traffic somewhere; seven ad formats and every ad view timed on the server.',
earners: 'Angle: for people who like daily click-to-earn sites; free credits every day and a streak, spent on your own campaign.',
honest: 'Angle: the honest one; not passive income, it pays for work, every payout is public.',
receipt: 'Angle: the receipt test; before you join anything, check three random payouts on the public ledger yourself.',
builder: 'Angle: for people who run a downline builder or a team; their members join under them and get a welcome credit.'
};
function buildMessages(kind, brief, angle, member) {
const k = KINDS[kind] || KINDS.post;
const sys = 'You write promotional copy for a member of InstantAdPay.\n\nWHAT IT IS: ' + FACTS + '\n\nVOICE: ' + VOICE + '\n\nCOMPLIANCE, the last word on everything: ' + COMPLIANCE
+ '\n\nMEMBER: username ' + (member.username || 'member') + '. Their invite link is ' + member.link + ' and it is the ONLY link you may use. ' + (ANGLES[angle] || ANGLES.plain)
+ '\n\nOUTPUT: ' + k.shape + ' Output the copy only: no preamble, no options, no quotes around it, no explanations.';
return [{ role: 'system', content: sys }, { role: 'user', content: 'Brief: ' + String(brief || 'no brief; write something a real member would post today').slice(0, 800) }];
}
function complete(messages) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ model: MODEL, max_tokens: 500, temperature: 0.8, messages });
const req = https.request({ hostname: 'openrouter.ai', path: '/api/v1/chat/completions', method: 'POST', headers: { Authorization: 'Bearer ' + key(), 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 45000 }, res => {
let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d).choices[0].message.content.trim()); } catch (e) { reject(new Error('engine ' + res.statusCode + ': ' + d.slice(0, 120))); } });
});
req.on('error', reject); req.on('timeout', () => req.destroy(new Error('engine timeout'))); req.end(body);
});
}
const scrub = t => String(t || '').replace(/—|--/g, ',').replace(/^["“]+|["”]+$/g, '').trim();
async function status(email) {
const acct = await R.accounts.byEmail(email); if (!acct) return { error: 'No such account.' };
const badges = await R.ads.milestonesOf(email);
const tier = tierFor(badges); const sc = R.siteConfig();
const u = usage()[email] || {}; const used = (u.months || {})[monthKey()] || 0;
const allowance = allowanceFor(tier, sc); const cost = Number(sc.aiCreditsPerGen) || 10;
let available = 0; try { const st = await R.ads.viewStatus(email); available = st.earnedAvailable != null ? st.earnedAvailable : (st.earned || 0); } catch (e) {}
const link = 'https://instantadpay.com/join/' + (acct.username || acct.code);
return { tier, badges, unlocked: ['surge', 'circuit', 'nexus'].includes(tier), engine: !!key(), allowance, used, freeLeft: Math.max(0, allowance - used), cost, available, link,
kinds: Object.entries(KINDS).map(([k, v]) => ({ key: k, label: v.label })), angles: Object.keys(ANGLES),
tiers: TIERS.map(t => ({ key: t.key, name: t.name, needText: t.needText || '', blurb: t.blurb, items: t.items, reached: t.key === 'free' || badges.includes(t.need) })),
history: (u.history || []).slice(-12).reverse() };
}
async function generate(email, kind, brief, angle) {
const st = await status(email); if (st.error) return st;
if (!st.unlocked) return { error: 'The AI Copy Engine unlocks at Surge: your first qualifying buyer of a $20 or larger package. Until then the ready-made posts and swipes are yours to use.' };
if (!st.engine) return { error: 'The engine is not configured on this server yet.' };
if (!KINDS[kind]) return { error: 'Pick what to write.' };
const acct = await R.accounts.byEmail(email);
let charged = 0;
if (st.freeLeft <= 0) {
if (st.available < st.cost) return { error: 'Your free generations for this month are used up and this one costs ' + st.cost + ' credits; you have ' + st.available + ' available. Earn a few on the Earn tab and come back.' };
if (!(await R.ads.spendEarned(email, st.cost))) return { error: 'Could not charge ' + st.cost + ' credits. Try again in a moment.' };
charged = st.cost;
}
let text;
try { text = scrub(await complete(buildMessages(kind, brief, angle, { username: acct.username, link: st.link }))); }
catch (e) { if (charged) { try { await R.ads.addEarned(email, charged); } catch (x) {} } return { error: 'The engine did not answer. Nothing was charged. Try again.' }; }
const u = usage(); const me = u[email] = u[email] || { months: {}, history: [] };
me.months[monthKey()] = (me.months[monthKey()] || 0) + 1;
me.history = (me.history || []).concat([{ ts: Date.now(), kind, angle: angle || 'plain', brief: String(brief || '').slice(0, 120), text: text.slice(0, 1500), charged }]).slice(-40);
saveUsage(u);
return { ok: true, text, kind, charged, freeLeft: Math.max(0, st.freeLeft - (charged ? 0 : 1)), cost: st.cost };
}
async function adminUsage() {
const u = usage(); const m = monthKey(); const rows = [];
for (const [email, v] of Object.entries(u)) rows.push({ email, month: (v.months || {})[m] || 0, total: Object.values(v.months || {}).reduce((a, b) => a + b, 0), last: (v.history || []).slice(-1)[0] || null });
return { month: m, rows: rows.sort((a, b) => b.month - a.month) };
}
module.exports = { init, status, generate, adminUsage, TIERS, KINDS };