Welcome credits (spec 8b earned pool v1) + official rehearsal rate card

Every account gets welcomeCredits (25, rate-card configurable) via an
idempotent lazy grant on dashboard load, so existing members receive theirs
retroactively. earned_credits table with JSON fallback; Overview stat shows
the purchased/welcome split. Chatbot facts and canned answers synced;
homepage free-membership list mentions the welcome batch. Rate card
promoted from placeholder to decided and persisted in the volume.
Spending integration for the earned pool is the next engine build.
Assets v=20260905b.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-05 05:48:42 -05:00
parent 013a88b7d0
commit d5bb74db7a
9 changed files with 73 additions and 25 deletions
+40 -2
View File
@@ -20,7 +20,8 @@ function rates() {
bannerBatch: 10, bannerCreditsPerBatch: 2,
textBatch: 10, textCreditsPerBatch: 1,
loginCreditsPerDay: 100,
burnBatchMin: 50
burnBatchMin: 50,
welcomeCredits: 25 // PLACEHOLDER until Marty sets the rate card (spec §8b earned pool)
}, saved);
}
function setRates(patch) {
@@ -240,6 +241,43 @@ const D = {
}
};
// ---- earned/welcome credits (spec §8b second pool: engine-side, never
// on-chain; welcome grant is idempotent and lazy so existing accounts get
// theirs on next dashboard load) ----
const EJ = {
db: null,
FILE: () => path.join(DATA_DIR, 'earned.json'),
load() { try { this.db = JSON.parse(fs.readFileSync(this.FILE(), 'utf8')); } catch (e) { this.db = {}; } },
save() { try { fs.writeFileSync(this.FILE(), JSON.stringify(this.db)); } catch (e) {} }
};
async function earnedBalance(email) {
const e = String(email || '').toLowerCase();
if (!e) return 0;
if (db.enabled()) {
const r = await db.q('SELECT balance FROM earned_credits WHERE email=?', [e]);
return r.length ? r[0].balance : 0;
}
if (!EJ.db) EJ.load();
return (EJ.db[e] && EJ.db[e].balance) || 0;
}
async function grantWelcome(email) {
const e = String(email || '').toLowerCase();
if (!e) return 0;
const amount = rates().welcomeCredits || 0;
if (db.enabled()) {
await db.q(`INSERT INTO earned_credits (email,balance,granted_welcome,updated) VALUES (?,?,1,?)
ON DUPLICATE KEY UPDATE balance = balance + IF(granted_welcome=0, ?, 0),
granted_welcome = 1, updated = VALUES(updated)`, [e, amount, Date.now(), amount]);
return earnedBalance(e);
}
if (!EJ.db) EJ.load();
if (!EJ.db[e] || !EJ.db[e].welcomed) {
EJ.db[e] = { balance: ((EJ.db[e] && EJ.db[e].balance) || 0) + amount, welcomed: true };
EJ.save();
}
return (EJ.db[e] && EJ.db[e].balance) || 0;
}
const impl = () => db.enabled() ? D : J;
function init(opts) { DATA_DIR = opts.dataDir; chain = opts.chain; J.load(); }
@@ -266,4 +304,4 @@ async function pendingBurns() { return impl().pendingBurns(); }
async function markBurned(id, tx) { return impl().markBurned(id, tx); }
module.exports = { init, rates, setRates, createCampaign, listCampaigns, setStatus,
serve, click, dailySweep, availableCredits, pendingBurns, markBurned };
serve, click, dailySweep, availableCredits, earnedBalance, grantWelcome, pendingBurns, markBurned };
+2 -2
View File
@@ -34,7 +34,7 @@ const CANNED = [
{ re: /(referral link|invite link|share link|refer)/i,
a: 'You get your share link the moment you sign in, free members included. One tip: switch on payouts (one free wallet step in Members) before your people start buying, because the contract locks each buyer to their sponsor at their first purchase.' },
{ re: /(credit|impression|cpm|what do i get|what am i buying)/i,
a: 'Packages mint ad credits on-chain. One credit is one cent of ad delivery: banners, text ads, and login ads across the network, managed from the campaign manager in Members. Only your campaigns can spend your credits.' },
a: 'Packages mint ad credits on-chain, and every new member also gets a small batch of welcome credits just for joining. One credit is one cent of ad delivery: banners, text ads, and login ads across the network, managed from the campaign manager in Members. Only your campaigns can spend your credits.' },
{ re: /(price|cost|package|how much is)/i,
a: 'Packages run 5 to 250 dollars, priced in dollars and settled in POL at the live rate when you buy. The full ladder with live pricing is on the home page. Packages of $20 or more count toward qualification.' },
{ re: /(safe|trust|rug|run away|company disappear)/i,
@@ -46,7 +46,7 @@ function systemPrompt() {
return `You are the assistant on InstantAdPay (https://instantadpay.com), a membership advertising platform.
FACTS:
- Free to join with email only (6-digit code sign-in, no passwords). Wallet appears only at purchase or payout activation.
- Free to join with email only (6-digit code sign-in, no passwords). Wallet appears only at purchase or payout activation. Every new member gets a small welcome batch of ad credits at signup (engine-side bonus credits, separate from on-chain purchased credits).
- Ad packages: Micro $5/500 credits, Activation $20/2,000, Builder $50/5,500, Growth $100/12,000, Leader $250/32,500. Dollar-priced, settled in POL (Polygon) at the live Chainlink rate. 1 credit = 1 cent of ad delivery.
- Live formats: display banners (per impression), text ads (per impression), login ads (per day). Coming: inbox ads, featured rotation with disclosed rotation size, verified-visit packs.
- Every purchase is split by an immutable smart contract in the same transaction: 50% direct sponsor, 20% level 2, 10% level 3, 20% platform. No withdrawals exist; money lands in members' own wallets instantly.
+6
View File
@@ -64,6 +64,12 @@ async function bootstrap() {
created BIGINT NOT NULL,
INDEX (owner_email), INDEX (type, status), INDEX (member_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS earned_credits (
email VARCHAR(190) PRIMARY KEY,
balance INT NOT NULL DEFAULT 0,
granted_welcome TINYINT NOT NULL DEFAULT 0,
updated BIGINT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS burns (
id VARCHAR(32) PRIMARY KEY,
member_id INT NOT NULL,
+3 -1
View File
@@ -30,7 +30,9 @@
try {
const d = await (await fetch('/api/my/dashboard')).json();
if (d.error) return;
$('dbCredits').textContent = (d.credits || 0).toLocaleString();
const wc = d.welcomeCredits || 0;
$('dbCredits').textContent = ((d.credits || 0) + wc).toLocaleString();
$('dbCreditsSub').textContent = wc ? (d.credits || 0).toLocaleString() + ' purchased · ' + wc + ' welcome' : '';
$('dbEarned').textContent = IAP.fmtPol(d.earnedWei || '0');
$('dbBuyers').textContent = d.buyerCount || 0;
$('dbTeam').textContent = (d.referrals || []).length;
+4 -4
View File
@@ -5,7 +5,7 @@
<title>The contract | InstantAdPay</title>
<meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself.">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260905a">
<link rel="stylesheet" href="/assets/site.css?v=20260905b">
</head>
<body>
<div class="wrap">
@@ -129,8 +129,8 @@
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
</footer>
</div>
<script src="/assets/common.js?v=20260905a"></script>
<script src="/assets/contract.js?v=20260905a"></script>
<script src="/assets/chat.js?v=20260905a"></script>
<script src="/assets/common.js?v=20260905b"></script>
<script src="/assets/contract.js?v=20260905b"></script>
<script src="/assets/chat.js?v=20260905b"></script>
</body>
</html>
+6 -5
View File
@@ -5,7 +5,7 @@
<title>InstantAdPay: advertise and earn, locked in code</title>
<meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger.">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260905a">
<link rel="stylesheet" href="/assets/site.css?v=20260905b">
</head>
<body>
@@ -255,6 +255,7 @@
<h3>Free membership includes</h3>
<ul class="checks">
<li>A member account and the live ledger</li>
<li>Welcome credits to taste real ad delivery</li>
<li>Your personal referral link, working from day one</li>
<li>Earnings from day one on your referrals' package purchases</li>
<li>Access to the member dashboard</li>
@@ -398,9 +399,9 @@
</div>
</section>
<script src="/assets/common.js?v=20260905a"></script>
<script src="/assets/wallet.js?v=20260905a"></script>
<script src="/assets/home.js?v=20260905a"></script>
<script src="/assets/chat.js?v=20260905a"></script>
<script src="/assets/common.js?v=20260905b"></script>
<script src="/assets/wallet.js?v=20260905b"></script>
<script src="/assets/home.js?v=20260905b"></script>
<script src="/assets/chat.js?v=20260905b"></script>
</body>
</html>
+4 -4
View File
@@ -5,7 +5,7 @@
<title>Live ledger | InstantAdPay</title>
<meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line.">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260905a">
<link rel="stylesheet" href="/assets/site.css?v=20260905b">
</head>
<body>
<div class="wrap">
@@ -25,8 +25,8 @@
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
</footer>
</div>
<script src="/assets/common.js?v=20260905a"></script>
<script src="/assets/ledger.js?v=20260905a"></script>
<script src="/assets/chat.js?v=20260905a"></script>
<script src="/assets/common.js?v=20260905b"></script>
<script src="/assets/ledger.js?v=20260905b"></script>
<script src="/assets/chat.js?v=20260905b"></script>
</body>
</html>
+6 -6
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Member area | InstantAdPay</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="stylesheet" href="/assets/site.css?v=20260905a">
<link rel="stylesheet" href="/assets/site.css?v=20260905b">
</head>
<body class="bo-body">
@@ -80,7 +80,7 @@
<div class="pane" id="pane-overview">
<div class="stats" style="margin-top:0">
<div class="stat"><div class="n" id="dbCredits">0</div><div class="l">Ad credits</div></div>
<div class="stat"><div class="n" id="dbCredits">0</div><div class="l">Ad credits</div><div class="small muted" id="dbCreditsSub"></div></div>
<div class="stat"><div class="n mono" id="dbEarned">0</div><div class="l">POL earned</div></div>
<div class="stat"><div class="n" id="dbBuyers">0</div><div class="l">Qualifying buyers</div></div>
<div class="stat"><div class="n" id="dbTeam">0</div><div class="l">Joined your line</div></div>
@@ -208,9 +208,9 @@
</div>
</div>
<script src="/assets/common.js?v=20260905a"></script>
<script src="/assets/wallet.js?v=20260905a"></script>
<script src="/assets/my.js?v=20260905a"></script>
<script src="/assets/chat.js?v=20260905a"></script>
<script src="/assets/common.js?v=20260905b"></script>
<script src="/assets/wallet.js?v=20260905b"></script>
<script src="/assets/my.js?v=20260905b"></script>
<script src="/assets/chat.js?v=20260905b"></script>
</body>
</html>
+2 -1
View File
@@ -316,7 +316,8 @@ const server = http.createServer(async (req, res) => {
const out = { memberId, email: s.email || (acct && acct.email) || null,
address: s.address || (acct && acct.address) || null,
refCode: (acct && acct.code) || null, credits: 0, buyerCount: 0,
earnedWei: '0', earnCount: 0, referrals: [] };
earnedWei: '0', earnCount: 0, referrals: [], welcomeCredits: 0 };
if (out.email) out.welcomeCredits = await ads.grantWelcome(out.email); // idempotent lazy grant
if (memberId) {
try {
const mm = await chain.member(memberId);