← back to Waitline

server.js

209 lines

#!/usr/bin/env node
/**
 * Waitline — sponsored status line for AI-agent wait states (Kickbacks.ai clone).
 * Zero-dependency Node server: ad rotation, impression/click ledger, 50% user
 * rev-share accounting, basic-auth admin dashboard.
 *
 * Integration: Claude Code statusLine command (statusline.sh) polls /api/ad.
 * No patching of Claude Code binaries — sanctioned hook only.
 */
const http = require("http");
const fs = require("fs");
const path = require("path");

const PORT = process.env.PORT || 9718;
const DATA = path.join(__dirname, "data");
const ADS_FILE = path.join(DATA, "ads.json");
const LEDGER = path.join(DATA, "ledger.jsonl");
const AUTH_USER = process.env.ADMIN_USER || "admin";
const AUTH_PASS = process.env.ADMIN_PASS || "DW2024!";
const USER_SPLIT = 0.5; // 50% of gross accrues to the user, like upstream

fs.mkdirSync(DATA, { recursive: true });
if (!fs.existsSync(LEDGER)) fs.writeFileSync(LEDGER, "");

function loadAds() {
  try { return JSON.parse(fs.readFileSync(ADS_FILE, "utf8")); } catch { return []; }
}
function saveAds(ads) { fs.writeFileSync(ADS_FILE, JSON.stringify(ads, null, 2)); }

function appendLedger(ev) {
  fs.appendFileSync(LEDGER, JSON.stringify(ev) + "\n");
}
function readLedger() {
  const raw = fs.readFileSync(LEDGER, "utf8").trim();
  if (!raw) return [];
  return raw.split("\n").map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
}

// Weighted random pick by CPM among active ads.
function pickAd(ads) {
  const active = ads.filter((a) => a.active);
  if (!active.length) return null;
  const total = active.reduce((s, a) => s + (a.cpm_usd || 0.01), 0);
  let r = Math.random() * total;
  for (const a of active) {
    r -= a.cpm_usd || 0.01;
    if (r <= 0) return a;
  }
  return active[active.length - 1];
}

function totals(events) {
  let gross = 0, user = 0, impressions = 0, clicks = 0;
  const byAd = {};
  for (const e of events) {
    const b = (byAd[e.ad_id] ||= { impressions: 0, clicks: 0, gross: 0, user: 0 });
    if (e.type === "impression") { impressions++; b.impressions++; }
    if (e.type === "click") { clicks++; b.clicks++; }
    gross += e.gross_usd || 0; user += e.user_share_usd || 0;
    b.gross += e.gross_usd || 0; b.user += e.user_share_usd || 0;
  }
  return { gross, user, impressions, clicks, byAd };
}

function json(res, code, obj) {
  res.writeHead(code, { "Content-Type": "application/json" });
  res.end(JSON.stringify(obj));
}

function requireAuth(req, res) {
  const h = req.headers.authorization || "";
  const ok = h.startsWith("Basic ") &&
    Buffer.from(h.slice(6), "base64").toString() === `${AUTH_USER}:${AUTH_PASS}`;
  if (!ok) {
    res.writeHead(401, { "WWW-Authenticate": 'Basic realm="Waitline"' });
    res.end("auth required");
  }
  return ok;
}

const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
const fmtDate = (iso) => new Date(iso).toLocaleString(undefined, {
  year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
});

function adminPage(url) {
  const ads = loadAds();
  const events = readLedger();
  const t = totals(events);
  const filterAd = url.searchParams.get("ad");
  const shown = filterAd ? events.filter((e) => e.ad_id === filterAd) : events;
  const recent = shown.slice(-50).reverse();

  const rows = ads.map((a) => {
    const b = t.byAd[a.id] || { impressions: 0, clicks: 0, gross: 0, user: 0 };
    return `<tr>
      <td><a href="/admin?ad=${esc(a.id)}">${esc(a.id)}</a></td>
      <td>${esc(a.sponsor)}</td>
      <td>${esc(a.text)}</td>
      <td>$${(a.cpm_usd || 0).toFixed(2)}</td>
      <td><a href="/admin?ad=${esc(a.id)}">${b.impressions}</a></td>
      <td><a href="/admin?ad=${esc(a.id)}">${b.clicks}</a></td>
      <td>$${b.gross.toFixed(4)}</td>
      <td>$${b.user.toFixed(4)}</td>
      <td>${a.active ? "🟢" : "⚫"}</td>
      <td class="when" title="${esc(a.created_at)}">🕓 ${fmtDate(a.created_at)}</td>
    </tr>`;
  }).join("");

  const ledgerRows = recent.map((e) => `<tr>
      <td class="when" title="${esc(e.ts)}">🕓 ${fmtDate(e.ts)}</td>
      <td>${esc(e.type)}</td>
      <td><a href="/admin?ad=${esc(e.ad_id)}">${esc(e.ad_id)}</a></td>
      <td>${esc(e.session || "-")}</td>
      <td>$${(e.gross_usd || 0).toFixed(5)}</td>
      <td>$${(e.user_share_usd || 0).toFixed(5)}</td>
    </tr>`).join("");

  return `<!doctype html><html><head><meta charset="utf-8"><title>Waitline Admin</title>
<style>
  body{font-family:-apple-system,system-ui,sans-serif;margin:24px;background:#0f1722;color:#dbe7e0}
  h1{font-size:22px} h2{font-size:16px;margin-top:28px;color:#7fd6a4}
  .cards{display:flex;gap:14px;flex-wrap:wrap;margin:16px 0}
  .card{background:#17222f;border:1px solid #23364a;border-radius:10px;padding:14px 18px;min-width:130px}
  .card .n{font-size:22px;font-weight:700;color:#2aa44f} .card .l{font-size:12px;color:#8a96a7}
  table{border-collapse:collapse;width:100%;font-size:13px}
  th,td{border-bottom:1px solid #23364a;padding:6px 10px;text-align:left}
  th{color:#8a96a7;font-weight:600} a{color:#5bc98a;text-decoration:none} a:hover{text-decoration:underline}
  .when{color:#8a96a7;font-size:12px;white-space:nowrap}
  .filter{background:#2a1f0f;border:1px solid #6b4e12;padding:6px 12px;border-radius:8px;display:inline-block;margin-bottom:10px}
</style></head><body>
<h1>Waitline — sponsored wait-state admin</h1>
${filterAd ? `<div class="filter">Filtered to ad <b>${esc(filterAd)}</b> — <a href="/admin">clear</a></div>` : ""}
<div class="cards">
  <div class="card"><div class="n"><a href="/admin">$${t.gross.toFixed(4)}</a></div><div class="l">gross revenue</div></div>
  <div class="card"><div class="n"><a href="/admin">$${t.user.toFixed(4)}</a></div><div class="l">user share (50%)</div></div>
  <div class="card"><div class="n"><a href="/admin">${t.impressions}</a></div><div class="l">impressions</div></div>
  <div class="card"><div class="n"><a href="/admin">${t.clicks}</a></div><div class="l">clicks</div></div>
</div>
<h2>Ad inventory</h2>
<table><tr><th>id</th><th>sponsor</th><th>line</th><th>CPM</th><th>impr</th><th>clicks</th><th>gross</th><th>user</th><th>live</th><th>created</th></tr>${rows}</table>
<h2>Ledger (last 50${filterAd ? ", filtered" : ""})</h2>
<table><tr><th>when</th><th>type</th><th>ad</th><th>session</th><th>gross</th><th>user share</th></tr>${ledgerRows}</table>
</body></html>`;
}

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://localhost:${PORT}`);

  if (url.pathname === "/healthz") return json(res, 200, { ok: true, service: "waitline" });

  if (url.pathname === "/api/ad") {
    const ads = loadAds();
    const ad = pickAd(ads);
    if (!ad) return json(res, 200, { line: null });
    const gross = (ad.cpm_usd || 0) / 1000;
    appendLedger({
      ts: new Date().toISOString(), type: "impression", ad_id: ad.id,
      session: url.searchParams.get("session") || null,
      gross_usd: gross, user_share_usd: gross * USER_SPLIT,
    });
    const t = totals(readLedger());
    return json(res, 200, {
      id: ad.id, sponsor: ad.sponsor, line: ad.text, url: ad.url,
      earned_total_usd: +t.user.toFixed(5),
    });
  }

  if (url.pathname === "/api/click" && req.method === "POST") {
    let body = "";
    req.on("data", (c) => (body += c));
    req.on("end", () => {
      let adId = null;
      try { adId = JSON.parse(body).ad_id; } catch {}
      const ad = loadAds().find((a) => a.id === adId);
      if (!ad) return json(res, 404, { error: "unknown ad" });
      const gross = ad.cpc_usd || 0.05;
      appendLedger({
        ts: new Date().toISOString(), type: "click", ad_id: ad.id,
        gross_usd: gross, user_share_usd: gross * USER_SPLIT,
      });
      json(res, 200, { ok: true });
    });
    return;
  }

  if (url.pathname === "/api/stats") {
    const t = totals(readLedger());
    return json(res, 200, {
      gross_usd: +t.gross.toFixed(5), user_usd: +t.user.toFixed(5),
      impressions: t.impressions, clicks: t.clicks, split: USER_SPLIT,
    });
  }

  if (url.pathname === "/api/ads") return json(res, 200, loadAds());

  if (url.pathname === "/admin" || url.pathname === "/") {
    if (!requireAuth(req, res)) return;
    res.writeHead(200, { "Content-Type": "text/html" });
    return res.end(adminPage(url));
  }

  json(res, 404, { error: "not found" });
});

server.listen(PORT, "127.0.0.1", () =>
  console.log(`waitline listening on http://127.0.0.1:${PORT}`)
);