Ad engine v1: banner/text/login campaigns against on-chain credits
Spec 8b types 1-3. Spend accrues per campaign in batches; burns queue for the engine signer (admin runs consume() on-chain, /api/admin/burns). Rates are volume config (adrates.json), rehearsal placeholders until Marty sets the real card. Public slots serve on the ledger page; campaign manager in the members area. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ const { URL } = require('url');
|
||||
const chain = require('./chain');
|
||||
const auth = require('./auth');
|
||||
const accounts = require('./accounts');
|
||||
const ads = require('./ads');
|
||||
|
||||
const PORT = Number(process.env.PORT || 3000);
|
||||
const ROOT = __dirname;
|
||||
@@ -24,6 +25,9 @@ fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
chain.init({ onEvent: ev => pushFeed(ev) });
|
||||
auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' });
|
||||
accounts.init({ dataDir: DATA_DIR });
|
||||
ads.init({ dataDir: DATA_DIR, chain });
|
||||
setTimeout(() => ads.dailySweep(), 60 * 1000);
|
||||
setInterval(() => ads.dailySweep(), 60 * 60 * 1000); // login-ad daily charges
|
||||
|
||||
function siteConfig() {
|
||||
let saved = {};
|
||||
@@ -216,7 +220,60 @@ const server = http.createServer(async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// -- ad engine (spec §8b v1: banners, text, login ads)
|
||||
if (p === '/api/ads/slot' && req.method === 'GET') {
|
||||
const ad = ads.serve(String(u.searchParams.get('type') || 'banner'));
|
||||
return json(res, 200, { ad });
|
||||
}
|
||||
m = /^\/api\/ads\/click\/(\d+)$/.exec(p);
|
||||
if (m && req.method === 'GET') {
|
||||
const target = ads.click(m[1]);
|
||||
if (!target) { res.writeHead(404, baseHeaders()); return res.end(); }
|
||||
res.writeHead(302, baseHeaders({ Location: target }));
|
||||
return res.end();
|
||||
}
|
||||
if (p === '/api/my/campaigns' && req.method === 'GET') {
|
||||
const s = auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
const memberId = await auth.refreshMemberId(s);
|
||||
const out = { campaigns: ads.listCampaigns(s.email), rates: ads.rates() };
|
||||
out.availableCredits = memberId ? await ads.availableCredits(memberId) : 0;
|
||||
return json(res, 200, out);
|
||||
}
|
||||
if (p === '/api/my/campaigns' && req.method === 'POST') {
|
||||
const s = auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
const memberId = await auth.refreshMemberId(s);
|
||||
if (!memberId) return json(res, 400, { error: 'Buy an ad package first. Campaigns spend the on-chain credits it mints.' });
|
||||
const b = await readBody(req);
|
||||
const r = await ads.createCampaign(s.email, memberId, b);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
m = /^\/api\/my\/campaigns\/(\d+)\/(pause|resume)$/.exec(p);
|
||||
if (m && req.method === 'POST') {
|
||||
const s = auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
const r = ads.setStatus(s.email, m[1], m[2] === 'pause' ? 'paused' : 'active');
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
|
||||
// -- admin (Bearer ADMIN_PASSWORD)
|
||||
if (p === '/api/admin/burns' && req.method === 'GET') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
return json(res, 200, { pending: ads.pendingBurns() });
|
||||
}
|
||||
if (p === '/api/admin/burns/mark' && req.method === 'POST') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
const r = ads.markBurned(b.id, b.tx);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
if (p === '/api/admin/rates' && req.method === 'PATCH') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
return json(res, 200, { ok: true, rates: ads.setRates(b) });
|
||||
}
|
||||
|
||||
if (p === '/api/admin/site' && req.method === 'PATCH') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
|
||||
Reference in New Issue
Block a user