Balances: one reservation rule (earned first) + purchased-grade credited pool
- balances(): live campaign budgets are set aside up front, earned pool first then purchased, so dashboard numbers never move per impression; new 'in campaigns' line - earned pool gains a purchase_grade portion for refunds/comps: shows under Purchased as 'credited to you' and funds login ads (daily login charge draws on it first) - viewing credits always spend before credited purchased money - dashboard/donut/spend banner/earn pane/positions show purchased, credited, earned, in campaigns - chatbot canned answer + AI prompt: the balance rule Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -782,7 +782,8 @@ const server = http.createServer(async (req, res) => {
|
||||
const mm = await chain.member(memberId);
|
||||
out.buyerCount = mm.buyerCount;
|
||||
out.onchainSponsorId = mm.sponsorId;
|
||||
out.credits = (await ads.pooledCredits((await myMemberIds(s)).ids)).total;
|
||||
const bal = await ads.balances((await myMemberIds(s)).ids, s.email);
|
||||
out.credits = bal.total; out.creditedCredits = bal.credited; out.earnedCredits = bal.earned; out.inCampaigns = bal.inCampaigns; out.availableCredits = bal.available;
|
||||
} catch (e) { out.chainReadError = true; }
|
||||
}
|
||||
return json(res, 200, out);
|
||||
@@ -831,7 +832,8 @@ const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const mm = await chain.member(memberId);
|
||||
out.buyerCount = mm.buyerCount;
|
||||
out.credits = (await ads.pooledCredits((await myMemberIds(s)).ids)).total;
|
||||
const bal = await ads.balances((await myMemberIds(s)).ids, s.email);
|
||||
out.credits = bal.total; out.creditedCredits = bal.credited; out.earnedCredits = bal.earned; out.inCampaigns = bal.inCampaigns; out.availableCredits = bal.available;
|
||||
} catch (e) { out.chainReadError = true; }
|
||||
let earned = 0n, n = 0;
|
||||
for (const ev of chain.recentEvents(600)) {
|
||||
@@ -878,19 +880,21 @@ const server = http.createServer(async (req, res) => {
|
||||
const mainId = await auth.refreshMemberId(s);
|
||||
const list = await accounts.positions(s.email);
|
||||
const out = [];
|
||||
const posIds = [mainId, ...list.map(p => p.memberId)].filter(Boolean);
|
||||
let balPer = {}, credited = 0; try { const bb = await ads.balances(posIds, s.email); credited = bb.credited; for (const p of bb.per) balPer[p.memberId] = p.avail; } catch (e) {}
|
||||
for (const pos of list) {
|
||||
let id = pos.memberId;
|
||||
if (!id) { try { id = await chain.memberIdByAccount(pos.address); if (id) await accounts.setPositionMember(pos.address, id); } catch (e) {} }
|
||||
const row = { address: pos.address, memberId: id || 0, buyerCount: 0, counted: false, credits: 0, created: pos.created };
|
||||
if (id) {
|
||||
try { const mm = await chain.member(id); row.buyerCount = mm.buyerCount; row.counted = mm.countedAsBuyer; row.sponsorId = mm.sponsorId; } catch (e) {}
|
||||
try { row.credits = await ads.availableCredits(id); } catch (e) {}
|
||||
row.credits = balPer[id] != null ? balPer[id] : 0;
|
||||
}
|
||||
out.push(row);
|
||||
}
|
||||
const main = { address: (acct && acct.address) || null, memberId: mainId, credits: 0, buyerCount: 0 };
|
||||
if (mainId) {
|
||||
try { main.credits = await ads.availableCredits(mainId); } catch (e) {}
|
||||
main.credits = balPer[mainId] != null ? balPer[mainId] : 0;
|
||||
try { main.buyerCount = (await chain.member(mainId)).buyerCount; } catch (e) {}
|
||||
}
|
||||
// live POL balances (the link signature proved the wallet is theirs) + a POL/USD rate from the catalog
|
||||
@@ -898,7 +902,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (main.address) main.balanceWei = await bal(main.address);
|
||||
for (const row of out) row.balanceWei = await bal(row.address);
|
||||
let polUsd = 0; try { const cat = await chain.catalog(); const pk = cat.find(x => x.costWei); if (pk) polUsd = (pk.priceCents / 100) / (Number(BigInt(pk.costWei)) / 1e18); } catch (e) {}
|
||||
return json(res, 200, { main, positions: out, polUsd, totalCredits: main.credits + out.reduce((n, r) => n + r.credits, 0) });
|
||||
return json(res, 200, { main, positions: out, polUsd, totalCredits: main.credits + out.reduce((n, r) => n + r.credits, 0), credited });
|
||||
}
|
||||
if (p === '/api/my/positions/remove' && req.method === 'POST') {
|
||||
const s = await auth.fromRequest(req);
|
||||
@@ -1554,12 +1558,15 @@ const server = http.createServer(async (req, res) => {
|
||||
const memberId = await auth.refreshMemberId(s);
|
||||
const out = { campaigns: await ads.listCampaigns(s.email), rates: ads.rates(), bannerSizes: ads.bannerSizes() };
|
||||
out.clickSources = await coach.clickSources(out.campaigns.map(c => c.id));
|
||||
const pool = await ads.pooledCredits((await myMemberIds(s)).ids);
|
||||
const pool = await ads.balances((await myMemberIds(s)).ids, s.email);
|
||||
out.purchasedCredits = pool.total;
|
||||
out.largestPosition = pool.best.avail; // a single campaign budget has to fit one position
|
||||
out.positionCount = pool.per.length;
|
||||
out.earnedCredits = await ads.earnedBalance(s.email);
|
||||
out.availableCredits = out.purchasedCredits + out.earnedCredits;
|
||||
out.creditedCredits = pool.credited; // refunded/credited purchased money, free (counts inside purchasedCredits)
|
||||
out.earnedCredits = pool.earned; // earned pool minus what live campaigns already hold
|
||||
out.earnedReserved = pool.earnedReserved;
|
||||
out.inCampaigns = pool.inCampaigns; // budget still to deliver across live campaigns
|
||||
out.availableCredits = pool.available;
|
||||
return json(res, 200, out);
|
||||
}
|
||||
if (p === '/api/my/campaigns' && req.method === 'POST') {
|
||||
@@ -1573,13 +1580,14 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
// charge the best-funded of the member's positions (main + Qualified Start
|
||||
// wallets). A campaign burns from one member id, so the budget must fit inside it.
|
||||
const pool = await ads.pooledCredits((await myMemberIds(s)).ids);
|
||||
const ids = (await myMemberIds(s)).ids;
|
||||
const pool = await ads.balances(ids, s.email);
|
||||
const fundId = pool.best.memberId || memberId;
|
||||
const earnedNow = String(b.type) !== 'login' ? await ads.earnedBalance(s.email) : 0;
|
||||
const earnedNow = String(b.type) !== 'login' ? pool.earned : 0;
|
||||
const budget = Math.floor(Number(b.budget) || 0);
|
||||
if (pool.per.length > 1 && budget > pool.best.avail + earnedNow && budget <= pool.total + earnedNow)
|
||||
if (pool.per.length > 1 && budget > pool.best.avail + pool.credited + earnedNow && budget <= pool.total + earnedNow)
|
||||
return json(res, 400, { error: 'Your credits are spread across ' + pool.per.length + ' positions and one campaign spends from one of them. The largest single position holds ' + pool.best.avail + ' credits: set the budget to that or less, or run two campaigns.' });
|
||||
const r = await ads.createCampaign(s.email, fundId, b);
|
||||
const r = await ads.createCampaign(s.email, fundId, b, ids);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
m = /^\/api\/my\/campaigns\/(\d+)\/topup$/.exec(p);
|
||||
|
||||
Reference in New Issue
Block a user