// 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, coach, messages, promos, videomaker } 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: true }, { t: 'Printable handout with your QR', live: true }] }, { 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: true }, { t: 'Split tester for your join angles', live: true }] }, { 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: true }, { t: 'Co-branded pages and team credit grants', live: true }, { t: 'Your own partner kit and promo code', live: true }] } ]; 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.' }, broadcast: { label: 'Team broadcast (leaders)', shape: 'Write ONE message from a team leader to their whole team: first line "Subject: ..." then a blank line, then an 80-150 word body. One clear focus for this week (from the brief, or pick: share your link daily, view your ads, message two people), one useful tip, warm and direct, no pressure, no invented numbers. Sign off as the member. No link needed.' }, 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(/\s*[—–]\s*|\s*--\s*/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); const partner = tier === 'nexus' ? await partnerStatus(email) : null; const handoutUrl = rank[tier] >= rank.spark && acct.username ? 'https://instantadpay.com/handout/' + acct.username : null; return { tier, badges, username: acct.username || null, partner, handoutUrl, videoMaker: !!(R.videomaker && R.videomaker.available()), 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, { log: { kind: 'tool', note: 'AI Copy Engine: ' + kind } }))) 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, { log: { kind: 'refund', note: 'Refund: AI Copy Engine did not answer' } }); } 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) }; } // ---- the rest of the ladder (Marty, 2026-09-14): Spark templates + handout, Circuit split tester + Video // Maker, Nexus Leader Ops + team grants + a member-funded partner code ---- const KIT = 'https://instantadpay.com'; async function tierOf(email) { return tierFor(await R.ads.milestonesOf(email)); } const rank = { free: 0, spark: 1, surge: 2, circuit: 3, nexus: 4 }; const atLeast = (tier, need) => rank[tier] >= rank[need]; // Spark: one-tap campaigns aimed at the member's link, funded from what they already hold async function template(email, memberId, ids, kind, budget) { const tier = await tierOf(email); if (!atLeast(tier, 'spark')) return { error: 'Campaign templates unlock at Spark: switch on payouts first.' }; const acct = await R.accounts.byEmail(email); const link = KIT + '/join/' + (acct.username || acct.code); const b = Math.max(50, Math.min(5000, Math.floor(budget) || 200)); const T = { banner: { type: 'banner', name: 'My invite: banner 300x250', targetUrl: link, imageUrl: KIT + '/banners/iap-300x250.png', size: '300x250', budget: b }, leaderboard: { type: 'banner', name: 'My invite: banner 728x90', targetUrl: link, imageUrl: KIT + '/banners/iap-728x90.png', size: '728x90', budget: b }, text: { type: 'text', name: 'My invite: text ad', targetUrl: link, title: 'Paid the second someone buys', body: 'An ad platform that pays sponsors in the same transaction, on-chain. Free to join, credits for showing up.', budget: b }, text2: { type: 'text', name: 'My invite: text ad (free angle)', targetUrl: link + '?v=free', title: 'Run your first ad campaign for $0', body: 'Join free, view a few ads, earn credits, launch a campaign. Every payout is public on the ledger.', budget: b } }[kind]; if (!T) return { error: 'Pick a template.' }; const r = await R.ads.createCampaign(email, memberId || 0, T, ids); if (r.error) return r; return { ok: true, campaign: r.campaign || r, name: T.name, budget: b }; } // Spark: printable handout with the member's QR (public page so it can be shared or printed anywhere) async function handout(username) { const a = await R.accounts.byUsername(username); if (!a) return null; if (!atLeast(await tierOf(a.email), 'spark')) return null; const link = KIT + '/join/' + username; const qr = '/api/qr?d=' + encodeURIComponent(link); const esc = t => String(t || '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); return '
Two handouts per sheet. Cut along the gap.
' + [1, 2].map(() => '
Join free with your email. View a few ads a day and earn credits for your own campaign. When anyone you invite buys an ad package, the contract pays you instantly, wallet to wallet, on a public ledger.
Scan the code or go to
' + esc(link) + '
Your sponsor: @' + esc(username) + '