// 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 };