Credit activity ledger: every earn and spend logged with a reason, listed under Campaigns

Hugh earned a 10-credit sign-in bonus, then bought a 280-credit extension; earned credits spend
first, so Earned read 0 and the bonus looked lost. There was no record a member could check.
Now every real credit movement writes a credit_log row (JSON store in file mode): welcome, sign-in
bonus with streak, daily claim, inbox reads, video watches, verified visits, milestone/leaderboard
bonuses, partner codes, admin/team grants, AI Copy Engine charges + refunds, flat buys (featured
run, extension, visit pack, more visits), login-ad days, and metered delivery rolled up per
campaign per day; on-chain purchased-credit spends are marked. GET /api/my/credits/activity;
dashboard shows the last 40 under Campaigns (also when there are no campaigns) with a link from
Earn credits. Chatbot canned answer + AI prompt updated; my.js tag bumped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-16 08:41:43 -05:00
parent ffacf741da
commit 69d672665c
8 changed files with 50 additions and 14 deletions
+4 -4
View File
@@ -109,12 +109,12 @@ async function generate(email, kind, brief, angle) {
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.' };
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); } catch (x) {} } return { error: 'The engine did not answer. Nothing was charged. Try again.' }; }
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);
@@ -204,8 +204,8 @@ async function grant(fromEmail, toEmail, credits) {
if (!(await R.accounts.isDownlineOf(fromEmail, to, 3))) return { error: 'That member is not in your line.' };
let avail = 0; try { const st = await R.ads.viewStatus(fromEmail); avail = st.earnedAvailable != null ? st.earnedAvailable : (st.earned || 0); } catch (e) {}
if (avail < n) return { error: 'You have ' + avail + ' credits available to give; free some up or earn more first.' };
if (!(await R.ads.spendEarned(fromEmail, n))) return { error: 'Could not move the credits. Try again.' };
await R.ads.addEarned(to, n);
if (!(await R.ads.spendEarned(fromEmail, n, { log: { kind: 'grant', note: 'Team grant to ' + to } }))) return { error: 'Could not move the credits. Try again.' };
await R.ads.addEarned(to, n, { log: { kind: 'grant', note: 'Team grant from your sponsor line' } });
const a = await R.accounts.byEmail(fromEmail), b = await R.accounts.byEmail(to);
const fromName = a.username ? '@' + a.username : 'your sponsor', toName = b.username ? '@' + b.username : to.replace(/@.*/, '') + '@';
try { await R.messages.sendChat(a.memberId || 0, fromEmail, to, 'I just moved ' + n + ' ad credits to your account. Spend them on a campaign pointed at your invite link: Campaigns > New campaign.'); } catch (e) {}