Positions policy: cap of five per account, refuse wallets registered under anything but the main member, admin members shows positions; Qualified Start card states the rule

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-10 09:42:06 -05:00
parent f6490c9444
commit 94910275c0
4 changed files with 20 additions and 4 deletions
+1 -1
View File
@@ -306,6 +306,6 @@
</div>
<script src="/assets/common.js?v=20260910c"></script>
<script src="/assets/admin.js?v=20260910b"></script>
<script src="/assets/admin.js?v=20260910c"></script>
</body>
</html>
+2 -2
View File
@@ -276,11 +276,11 @@
const q = ($('memFilter').value || '').trim().toLowerCase();
const list = allMembers.filter(a => !q || [a.email, a.username, a.memberId, a.sponsorRef, a.address, a.code].join(' ').toLowerCase().includes(q));
$('memSub').textContent = list.length + ' of ' + allMembers.length;
$('memTable').innerHTML = '<tr><th>Email</th><th>Username</th><th>Member #</th><th>Wallet</th><th>Sponsor</th><th>Via</th><th>Code</th><th>Joined</th><th></th></tr>'
$('memTable').innerHTML = '<tr><th>Email</th><th>Username</th><th>Member #</th><th>Wallet</th><th>Sponsor</th><th>Positions</th><th>Via</th><th>Code</th><th>Joined</th><th></th></tr>'
+ list.map(a => '<tr><td>' + esc(a.email) + '</td><td>' + (a.username ? '@' + esc(a.username) : '<span class="muted">none</span>') + '</td>'
+ '<td>' + (a.memberId ? '#' + a.memberId : '<span class="muted">free</span>') + '</td>'
+ '<td class="mono small">' + (a.address ? esc(a.address.slice(0, 8) + '…' + a.address.slice(-6)) : '<span class="muted">none</span>') + '</td>'
+ '<td>' + esc(a.sponsorRef || '') + '</td><td class="small muted">' + esc(a.joinedVia || '') + '</td><td class="mono small">' + esc(a.code || '') + '</td>'
+ '<td>' + esc(a.sponsorRef || '') + '</td><td class="small" title="linked Qualified Start positions' + (a.positionIds && a.positionIds.length ? ': #' + a.positionIds.join(', #') : '') + '">' + (a.positions ? a.positions : '<span class="muted">0</span>') + '</td><td class="small muted">' + esc(a.joinedVia || '') + '</td><td class="mono small">' + esc(a.code || '') + '</td>'
+ '<td class="small muted">' + when(a.created) + '</td>'
+ '<td class="act"><button class="btn small sec" data-spon="' + esc(a.email) + '" data-cur="' + esc(a.sponsorRef || '') + '">Sponsor</button></td></tr>').join('');
}
+1
View File
@@ -373,6 +373,7 @@
<span class="small muted" id="qsHint"></span></p>
<div id="qsList" class="small"></div>
<p class="muted small" style="margin:12px 0 0">Your own money, your own wallets, a faster start. It is not an income promise: qualification only pays on future purchases in your line, and every purchase here is a real ad package you can spend.</p>
<p class="small" style="border:1px solid var(--line-strong);border-radius:10px;padding:8px 12px;margin:10px 0 0"><b>Positions are for qualifying.</b> Up to five per account, each registered directly under your main member. Once you are qualified, buy from your main wallet: that keeps your own sponsor paid in full, the same way your people's purchases pay you.</p>
</div>
<div class="card" id="qsCalcCard">
<h3>Qualified Start calculator</h3>
+16 -1
View File
@@ -717,6 +717,19 @@ const server = http.createServer(async (req, res) => {
// Qualified Start: a second (third…) wallet on the same account. It becomes
// its own on-chain member under this member's id when it buys; the session
// stays on the main wallet.
// policy (Marty, 2026-09-10): positions exist to qualify, so at most five per account, and a
// wallet that is already registered on-chain under anything other than this account's main
// member is refused (a position-under-position chain would recapture 70% of a self-buy)
const MAX_POS = 5;
const have = await accounts.positions(s.email);
if (!have.find(p => p.address === r.address.toLowerCase()) && have.length >= MAX_POS)
return json(res, 400, { error: 'You can link up to ' + MAX_POS + ' positions, which is everything Qualified Start needs. Once qualified, buy from your main wallet so your sponsor is paid in full.' });
if (memberId) {
const mainId = await auth.refreshMemberId(s);
let mm = null; try { mm = await chain.member(memberId); } catch (e) {}
if (mm && mainId && mm.sponsorId !== mainId)
return json(res, 400, { error: 'That wallet is already registered on-chain under ' + (mm.sponsorId ? 'member #' + mm.sponsorId : 'no sponsor') + ', not under your main member #' + mainId + '. Only positions registered directly under you can be linked.' });
}
const pr = await accounts.addPosition(s.email, r.address);
if (pr.error) return json(res, 400, pr);
if (memberId) await accounts.setPositionMember(r.address, memberId);
@@ -1641,7 +1654,9 @@ const server = http.createServer(async (req, res) => {
}
if (p === '/api/admin/members' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { members: await accounts.listAll(500) });
const members = await accounts.listAll(500);
for (const m of members) { try { const ps = await accounts.positions(m.email); m.positions = ps.length; m.positionIds = ps.map(p => p.memberId).filter(Boolean); } catch (e) { m.positions = 0; } }
return json(res, 200, { members });
}
if (p === '/api/admin/members' && req.method === 'PATCH') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });