Holding tank: unsponsored free members, first-come adoption (own $20 buy required, 2 open, 7-day window, twice max), release to tank, admin view; PIF wallet-to-wallet POL gift with logging

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-11 07:57:44 -05:00
parent 9939c0487f
commit 9709ce782d
10 changed files with 320 additions and 5 deletions
+34
View File
@@ -24,6 +24,7 @@ const spaces = require('./spaces'); // DO Spaces video storage (inert unless DO_
let QR = null; try { QR = require('qrcode'); } catch (e) { /* optional */ }
const chatbot = require('./chatbot');
const coach = require('./coach'); // coaching view, nudges, digest, prospects, link stats
const tank = require('./tank'); // holding tank: unsponsored free members, adoptions, pay-it-forward
const burner = require('./burner'); // automatic on-chain credit burns (inert without ENGINE_KEY)
const PORT = Number(process.env.PORT || 3000);
@@ -301,6 +302,8 @@ async function boot() {
setInterval(() => ads.scheduleSweep().catch(e => console.error('schedule sweep', e.message)), 5 * 60 * 1000); // scheduled starts/ends
// follow-up email sequence: send whatever came due (every 10 min, first pass shortly after boot)
coach.init({ dataDir: DATA_DIR, chain, accounts, mailer });
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
setInterval(() => tank.sweep().catch(e => console.error('tank sweep', e.message)), 60 * 60 * 1000); // adoptions past their 7-day window
burner.init({ chain, ads });
setTimeout(() => coach.nudgeTick().catch(e => console.error('coach', e.message)), 90 * 1000);
setInterval(() => coach.nudgeTick().catch(e => console.error('coach', e.message)), 60 * 60 * 1000);
@@ -980,6 +983,37 @@ const server = http.createServer(async (req, res) => {
return json(res, r.error ? 400 : 200, r);
}
// -- coaching: every direct's ladder rung, stalled flag, and what to say
// -- holding tank: waiting members, my adoptions, adopt, release (pay it forward)
if (p === '/api/my/tank' && 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 tank.view(s.email));
}
if (p === '/api/my/tank/adopt' && 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 tank.adopt(s.email, b.who, b.note);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/tank/release' && 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 tank.release(s.email, b.email);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/gift' && req.method === 'POST') { // PIF: log a wallet-to-wallet POL gift and tell the recipient
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 tank.recordGift(s.email, b.email, b.tx, b.pol);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/tank' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await tank.adminView());
}
if (p === '/api/my/coach' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });