diff --git a/ads.js b/ads.js index bd51794..c0a4560 100644 --- a/ads.js +++ b/ads.js @@ -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 }; diff --git a/chatbot.js b/chatbot.js index 500a8a6..b4aec18 100644 --- a/chatbot.js +++ b/chatbot.js @@ -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. diff --git a/db.js b/db.js index 026e3d3..5d5be3f 100644 --- a/db.js +++ b/db.js @@ -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, diff --git a/public/assets/my.js b/public/assets/my.js index 4c3a574..373b4e0 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -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; diff --git a/public/contract.html b/public/contract.html index e6ab88e..4d665ec 100644 --- a/public/contract.html +++ b/public/contract.html @@ -5,7 +5,7 @@