diff --git a/ads.js b/ads.js index 0bd62ea..62faaec 100644 Binary files a/ads.js and b/ads.js differ diff --git a/chatbot.js b/chatbot.js index b1e77c6..87994b9 100644 --- a/chatbot.js +++ b/chatbot.js @@ -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//, 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. diff --git a/public/assets/admin.js b/public/assets/admin.js index 2352357..69bd9c1 100644 --- a/public/assets/admin.js +++ b/public/assets/admin.js @@ -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]) => '
' + esc(SITE_META[k] || humanize(k)) + '' diff --git a/public/assets/my.js b/public/assets/my.js index 990048e..41cc46d 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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 => '
' + esc(t.name) + (t.reached ? ' ✓' : '') + '
' + (t.reached ? esc(t.blurb) : 'Unlocks when you ' + esc(t.needText)) + '
    ' + t.items.map(i => '' + esc(i.t) + '').join('') + '
' + (t.reached ? '' : '
locked
') + '
').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 => '').join(''); $('tkAngle').innerHTML = r.angles.map(a => '').join(''); } + $('tkHistory').innerHTML = r.history.length ? '

Recent

' + r.history.map(h => '
' + esc(h.kind) + ' · ' + new Date(h.ts).toLocaleString() + (h.charged ? ' · ' + h.charged + ' credits' : ' · free') + '
' + esc(h.text).replace(/\n/g, '
') + '
').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 => { diff --git a/public/assets/site.css b/public/assets/site.css index 913bad1..6d48140 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -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} diff --git a/public/my.html b/public/my.html index 2bdce9e..b516ef2 100644 --- a/public/my.html +++ b/public/my.html @@ -5,7 +5,7 @@ Member area | InstantAdPay - + @@ -679,6 +679,7 @@ Open
+ @@ -687,6 +688,26 @@
+

Social posts, personalized with your link

Copy and paste, or post straight from here. Every post already carries your invite link, @@ -924,7 +945,7 @@ - + diff --git a/server.js b/server.js index a07809a..e1080d4 100644 --- a/server.js +++ b/server.js @@ -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/ @@ -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'; diff --git a/toolkit.js b/toolkit.js new file mode 100644 index 0000000..23b0ded --- /dev/null +++ b/toolkit.js @@ -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 };