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 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-04 14:00:43 -05:00
parent 4d909f8fd9
commit f3d2f7db88
8 changed files with 219 additions and 14 deletions
+18
View File
@@ -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') {