From f3d2f7db88ad724af82ddbacedbc7ac5198f8d7f Mon Sep 17 00:00:00 2001 From: martbost Date: Fri, 4 Sep 2026 14:00:43 -0500 Subject: [PATCH] 24/7 assistant: canned answers + OpenRouter AI, widget on every page RM Circle pattern: nine canned answers for the questions everyone asks (pyramid, withdrawals, splits, qualification, joining, credits, safety), OpenRouter fallback with a facts-loaded system prompt and hard no-income- promise rules, graceful no-key fallback. Floating mint bubble widget with linkified replies; 10/min per-IP rate limit. Assets v=20260904i. Co-Authored-By: Claude Fable 5 --- chatbot.js | 104 +++++++++++++++++++++++++++++++++++++++++ public/assets/chat.js | 58 +++++++++++++++++++++++ public/assets/site.css | 21 +++++++++ public/contract.html | 7 +-- public/index.html | 9 ++-- public/ledger.html | 7 +-- public/my.html | 9 ++-- server.js | 18 +++++++ 8 files changed, 219 insertions(+), 14 deletions(-) create mode 100644 chatbot.js create mode 100644 public/assets/chat.js diff --git a/chatbot.js b/chatbot.js new file mode 100644 index 0000000..500a8a6 --- /dev/null +++ b/chatbot.js @@ -0,0 +1,104 @@ +// 24/7 site assistant (RM Circle pattern): canned answers first for the +// questions everyone asks, OpenRouter AI for everything else, hard honesty +// rules baked into the system prompt. +// +// HOUSE RULE: whenever a site feature changes, update BOTH the CANNED +// answers and FACTS below in the same commit. +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +let DATA_DIR = null; +let chain = null; +const MODEL = process.env.OPENROUTER_MODEL || 'deepseek/deepseek-v4-flash:nitro'; + +function init(opts) { DATA_DIR = opts.dataDir; chain = opts.chain; } +function key() { + if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY.trim(); + try { return fs.readFileSync(path.join(DATA_DIR, 'openrouter.key'), 'utf8').trim(); } catch (e) { return ''; } +} +function enabled() { return !!key(); } + +// ---- canned answers: instant, free, always in voice ---- +const CANNED = [ + { re: /(pyramid|ponzi|scam|scheme)/i, + a: 'Fair question. A pyramid pays for recruiting with no product; here every payment is for real ad delivery, bought by real advertisers, and the whole money flow is public. Check the code and every payout yourself at https://instantadpay.com/contract and https://instantadpay.com/ledger.' }, + { re: /(withdraw|cash ?out|payout request|minimum payout)/i, + a: 'There are no withdrawals here, ever. The contract pays your share straight to your own wallet in the same transaction as the purchase. Nothing to request, nothing to wait on.' }, + { re: /(how (much|do i) earn|commission|percent|split)/i, + a: 'Every package splits the same way: 50 percent to the direct sponsor, 20 percent to level 2, 10 percent to level 3, 20 percent to the platform. Those numbers are constants in the contract and cannot be changed. Play with scenarios on the home page calculator. No income is promised; nobody earns unless real ad buying happens.' }, + { re: /(qualif|unlock level|level 2|level 3|pass.?up)/i, + a: 'Level 1 is open to every member. Bring 2 buyers of $20 or more and level 2 unlocks; 5 unlock level 3. When a level is not qualified, its share climbs the sponsor line, checking up to 25 positions, and pays the first qualified person. Qualification never expires and cannot be bought.' }, + { re: /(need (a )?wallet|crypto experience|metamask|how (do i|to) join|sign ?up|register)/i, + a: 'Join free with just your email at https://instantadpay.com/my, no wallet and no password needed. Your wallet only comes out when you buy a package or switch on payouts, and the site walks you through it.' }, + { re: /(referral link|invite link|share link|refer)/i, + a: 'You get your share link the moment you sign in, free members included. One tip: switch on payouts (one free wallet step in Members) before your people start buying, because the contract locks each buyer to their sponsor at their first purchase.' }, + { re: /(credit|impression|cpm|what do i get|what am i buying)/i, + a: 'Packages mint ad credits on-chain. One credit is one cent of ad delivery: banners, text ads, and login ads across the network, managed from the campaign manager in Members. Only your campaigns can spend your credits.' }, + { re: /(price|cost|package|how much is)/i, + a: 'Packages run 5 to 250 dollars, priced in dollars and settled in POL at the live rate when you buy. The full ladder with live pricing is on the home page. Packages of $20 or more count toward qualification.' }, + { re: /(safe|trust|rug|run away|company disappear)/i, + a: 'The money never touches us. An immutable contract splits every purchase in the same transaction, holds zero balance, and has no pause switch or upgrade path. Read the plain-language review and the verified source at https://instantadpay.com/contract.' } +]; + +// ---- facts for the AI fallback ---- +function systemPrompt() { + return `You are the assistant on InstantAdPay (https://instantadpay.com), a membership advertising platform. + +FACTS: +- Free to join with email only (6-digit code sign-in, no passwords). Wallet appears only at purchase or payout activation. +- Ad packages: Micro $5/500 credits, Activation $20/2,000, Builder $50/5,500, Growth $100/12,000, Leader $250/32,500. Dollar-priced, settled in POL (Polygon) at the live Chainlink rate. 1 credit = 1 cent of ad delivery. +- Live formats: display banners (per impression), text ads (per impression), login ads (per day). Coming: inbox ads, featured rotation with disclosed rotation size, verified-visit packs. +- Every purchase is split by an immutable smart contract in the same transaction: 50% direct sponsor, 20% level 2, 10% level 3, 20% platform. No withdrawals exist; money lands in members' own wallets instantly. +- Qualification: level 1 open to all; 2 buyers of $20+ unlock level 2; 5 unlock level 3. Unqualified shares pass up the sponsor line, checking up to 25 positions, else the platform receives them. Qualification cannot be bought and never expires. +- Every member gets a share link immediately (site code, /join/); the contract locks a buyer to their sponsor at the buyer's FIRST purchase, so members should switch on payouts before their referrals buy. +- Transparency: live ledger at /ledger streams every chain event with verify links; plain-language contract review at /contract; the verified source is public. +- The contract cannot be paused, upgraded, or drained. The operator can only manage the ad catalog ($1-$500 bounds, 24h price timelock) and rotate keys. + +RULES: +- 1 to 4 sentences, plain text, no markdown, include full URLs when pointing at a page. +- NEVER promise or estimate income. This is an advertising service with a referral program, never an investment. Be honest about risk: crypto transactions are irreversible, nobody earns unless real ad purchases happen. +- No em dashes. If you do not know, say so and point to /contract or the ledger.`; +} + +function askAI(message, historyText) { + return new Promise((resolve, reject) => { + const body = JSON.stringify({ + model: MODEL, + max_tokens: 300, + messages: [ + { role: 'system', content: systemPrompt() }, + ...(historyText ? [{ role: 'user', content: 'Earlier context: ' + historyText.slice(0, 800) }] : []), + { role: 'user', content: String(message).slice(0, 600) } + ] + }); + 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: 30000 }, + 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('openrouter ' + res.statusCode + ': ' + d.slice(0, 150))); } + }); + }); + req.on('error', reject); + req.on('timeout', () => req.destroy(new Error('openrouter timeout'))); + req.end(body); + }); +} + +async function answer(message, historyText) { + const msg = String(message || '').trim(); + if (!msg) return { error: 'Say something first.' }; + for (const c of CANNED) if (c.re.test(msg)) return { reply: c.a, canned: true }; + if (!enabled()) return { reply: 'Good question. The quick answers I have cover packages, payouts, qualification, and joining; for anything deeper, the plain-language contract review at https://instantadpay.com/contract covers most of it.' }; + try { return { reply: await askAI(msg, historyText) }; } + catch (e) { + console.error('chat AI failed', e.message); + return { reply: 'I hit a snag answering that one. The contract review at https://instantadpay.com/contract and the live ledger at https://instantadpay.com/ledger cover most deep questions.' }; + } +} + +module.exports = { init, answer, enabled }; diff --git a/public/assets/chat.js b/public/assets/chat.js new file mode 100644 index 0000000..5b69934 --- /dev/null +++ b/public/assets/chat.js @@ -0,0 +1,58 @@ +// 24/7 assistant widget: floating bubble, slide-up panel, /api/chat. +(function () { + const root = document.createElement('div'); + root.id = 'iapChat'; + root.innerHTML = '' + + ''; + document.body.appendChild(root); + + const $ = id => document.getElementById(id); + const msgs = $('iapChatMsgs'); + const input = $('iapChatIn'); + let history = []; + + const add = (text, who) => { + const d = document.createElement('div'); + d.className = 'ch-m ' + who; + // linkify plain URLs + d.innerHTML = String(text).replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c])) + .replace(/(https?:\/\/[^\s]+)/g, '$1'); + msgs.appendChild(d); + msgs.scrollTop = msgs.scrollHeight; + return d; + }; + async function send() { + const q = input.value.trim(); + if (!q) return; + input.value = ''; + add(q, 'me'); + const wait = add('…', 'bot'); + try { + const r = await (await fetch('/api/chat', { method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: q, history: history.slice(-4).join(' | ') }) })).json(); + wait.remove(); + add(r.reply || r.error || 'No answer came back. Try again.', 'bot'); + history.push('Q: ' + q, 'A: ' + (r.reply || '')); + } catch (e) { + wait.remove(); + add('Connection hiccup. Try that again.', 'bot'); + } + } + $('iapChatBtn').addEventListener('click', () => { + const p = $('iapChatPanel'); + p.hidden = !p.hidden; + if (!p.hidden) input.focus(); + }); + $('iapChatClose').addEventListener('click', () => { $('iapChatPanel').hidden = true; }); + $('iapChatSend').addEventListener('click', send); + input.addEventListener('keydown', e => { if (e.key === 'Enter') send(); }); +})(); diff --git a/public/assets/site.css b/public/assets/site.css index da357f6..6eadc43 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -220,6 +220,27 @@ input[type=range]{padding:0;border:0;background:transparent;accent-color:var(--m input:focus,select:focus{border-color:var(--mint)} :focus-visible{outline:2px solid var(--mint);outline-offset:2px} .hero-note{font-family:var(--mono);font-size:12px;color:var(--muted);margin-top:26px} +/* chat widget */ +#iapChatBtn{position:fixed;right:20px;bottom:20px;z-index:40;width:56px;height:56px;border-radius:50%; + border:0;background:var(--mint);color:var(--mint-ink);font-size:24px;cursor:pointer; + box-shadow:0 8px 30px rgba(67,232,195,.4)} +#iapChatBtn:hover{transform:scale(1.06)} +#iapChatPanel{position:fixed;right:20px;bottom:88px;z-index:40;width:min(360px,calc(100vw - 40px)); + background:var(--panel-solid);border:1px solid var(--line-strong);border-radius:18px;overflow:hidden; + box-shadow:0 24px 70px rgba(0,0,0,.65);display:flex;flex-direction:column} +.ch-head{padding:14px 16px;border-bottom:1px solid var(--line);font-family:var(--disp);position:relative} +.ch-head .ch-sub{display:block;font-size:12px;color:var(--muted);font-family:"Segoe UI",system-ui,sans-serif} +#iapChatClose{position:absolute;right:10px;top:10px;background:transparent;border:0;color:var(--muted); + font-size:22px;cursor:pointer;line-height:1} +.ch-msgs{padding:14px;overflow-y:auto;max-height:340px;display:flex;flex-direction:column;gap:10px} +.ch-m{border-radius:12px;padding:9px 13px;font-size:13.5px;line-height:1.5;max-width:86%} +.ch-m.bot{background:rgba(67,232,195,.08);border:1px solid rgba(67,232,195,.2);align-self:flex-start} +.ch-m.me{background:rgba(130,148,196,.12);border:1px solid var(--line);align-self:flex-end} +.ch-m a{word-break:break-all} +.ch-input{display:flex;gap:8px;padding:11px;border-top:1px solid var(--line)} +.ch-input input{flex:1;min-width:0} +.ch-input button{background:var(--mint);color:var(--mint-ink);border:0;border-radius:10px;padding:0 16px; + font-weight:700;cursor:pointer;font-family:var(--disp)} @media(prefers-reduced-motion:no-preference){ .feed .row:first-child{animation:landed .9s ease} @keyframes landed{from{background:rgba(67,232,195,.3)}to{background:var(--mint-soft)}} diff --git a/public/contract.html b/public/contract.html index 9bfd5e5..d118220 100644 --- a/public/contract.html +++ b/public/contract.html @@ -5,7 +5,7 @@ The contract | InstantAdPay - +
@@ -129,7 +129,8 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
- - + + + diff --git a/public/index.html b/public/index.html index c6b4c26..60196f0 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ InstantAdPay: advertise and earn, locked in code - + @@ -398,8 +398,9 @@ - - - + + + + diff --git a/public/ledger.html b/public/ledger.html index 4544b15..8506267 100644 --- a/public/ledger.html +++ b/public/ledger.html @@ -5,7 +5,7 @@ Live ledger | InstantAdPay - +
@@ -25,7 +25,8 @@
InstantAdPay · how it works · contract source ↗
- - + + + diff --git a/public/my.html b/public/my.html index 866fcc5..f949f82 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ My account | InstantAdPay - +
@@ -131,8 +131,9 @@
- - - + + + + diff --git a/server.js b/server.js index 7898da9..271bdd7 100644 --- a/server.js +++ b/server.js @@ -13,6 +13,7 @@ const auth = require('./auth'); const accounts = require('./accounts'); const ads = require('./ads'); const mailer = require('./mailer'); +const chatbot = require('./chatbot'); const PORT = Number(process.env.PORT || 3000); const ROOT = __dirname; @@ -28,6 +29,14 @@ auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' accounts.init({ dataDir: DATA_DIR }); ads.init({ dataDir: DATA_DIR, chain }); mailer.init({ dataDir: DATA_DIR }); +chatbot.init({ dataDir: DATA_DIR, chain }); +const chatHits = new Map(); +function chatLimited(ip) { + const now = Date.now(), rec = chatHits.get(ip); + if (!rec || now > rec.reset) { chatHits.set(ip, { count: 1, reset: now + 60000 }); return false; } + rec.count += 1; + return rec.count > 10; +} // magic-code sign-in: emailLower -> {code, exp, tries} const emailCodes = new Map(); setTimeout(() => ads.dailySweep(), 60 * 1000); @@ -170,6 +179,15 @@ const server = http.createServer(async (req, res) => { return json(res, 200, Object.assign({ onchainMembers: members, siteAccounts: accounts.count() }, chain.totals())); } + // -- 24/7 assistant + if (p === '/api/chat' && req.method === 'POST') { + const ip = req.socket.remoteAddress || 'x'; + if (chatLimited(ip)) return json(res, 429, { error: 'Give it a minute, then ask again.' }); + const b = await readBody(req); + const r = await chatbot.answer(b.message, b.history); + return json(res, r.error ? 400 : 200, r); + } + // -- accounts: email + password is the normal join path (wallet comes // out only at purchase / payout-activation time and gets linked then) if (p === '/api/signup' && req.method === 'POST') {