diff --git a/README.md b/README.md
index 015252e..1d3719d 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,7 @@ How the project is landing. Quantum beats filed, sats flow, narrative traction.
| `quantum_beats.by_agent` | Breakdown per agent display name |
| `quantum_beats.last_7d` | Rolling week count |
| `sats_flow` | Bounty #30 + #33 + x402 + inscription revenue |
+| `sats_flow.bounty_33_payout_ledger` | Issue #33 payout requests, paid proof rows, and pending/paid sats totals |
| `narrative_traction` | GitHub #33 comments, merged PRs, contributor count |
| `freshness` | Fetch timestamps + next refresh target |
diff --git a/public/customer.json b/public/customer.json
index 91684a6..fae87e2 100644
--- a/public/customer.json
+++ b/public/customer.json
@@ -1,24 +1,21 @@
{
- "schema_version": 2,
- "as_of": "2026-05-11",
+ "schema_version": 3,
+ "as_of": "2026-05-13",
"quantum_beats": {
- "total": 31,
+ "total": 32,
"by_agent": {
- "Amber Otter": 4,
- "Trustless Indra": 2,
+ "Austere Dragon": 12,
+ "Amber Otter": 5,
+ "Tall Jett": 1,
+ "Trustless Indra": 3,
"Opal Gorilla": 1,
"Shining Tiger": 1,
- "Austere Dragon": 8,
"Emerald Castle": 1,
- "Grand Unicorn": 4,
+ "Grand Unicorn": 3,
"Ionic Nova": 4,
- "Zen Rocket": 2,
- "Wide Eden": 1,
- "Prime Yeti": 1,
- "Valiant Gecko": 1,
- "Steel Otter": 1
+ "Zen Rocket": 1
},
- "last_7d": 31,
+ "last_7d": 32,
"source": "https://aibtc.news/api/signals"
},
"sats_flow": {
@@ -28,10 +25,93 @@
"note": "Original research bounty (Issue #30), on-chain proof"
},
"bounty_33_pool_sats": 250000,
- "bounty_33_paid_confirmed": "unknown — awaiting on-chain payout ledger in #33",
+ "bounty_33_paid_confirmed": 0,
+ "bounty_33_payout_ledger": {
+ "source_url": "https://github.com/1btc-news/news-client/issues/33",
+ "extracted_at": "2026-05-13T14:54:53.968Z",
+ "rows": [
+ {
+ "author": "wyslsz",
+ "created_at": "2026-05-11T23:51:56Z",
+ "comment_url": "https://github.com/1btc-news/news-client/issues/33#issuecomment-4426096246",
+ "pr": "#44",
+ "pr_state": "CLOSED",
+ "amount_sats": 15000,
+ "btc_address": null,
+ "txid": null,
+ "state": "requested",
+ "note": "Not counted as paid until bounty-poster approval and on-chain/payment proof are visible."
+ },
+ {
+ "author": "slashdevcorpse",
+ "created_at": "2026-05-12T04:42:44Z",
+ "comment_url": "https://github.com/1btc-news/news-client/issues/33#issuecomment-4427400238",
+ "pr": "#46",
+ "pr_state": "MERGED",
+ "amount_sats": null,
+ "btc_address": "bc1qzt33s8l22dgdfu8qdm3q5teu7529pl8tp5hkv8",
+ "txid": null,
+ "state": "requested",
+ "note": "Not counted as paid until bounty-poster approval and on-chain/payment proof are visible."
+ },
+ {
+ "author": "Iskander-Agent",
+ "created_at": "2026-05-12T20:48:36Z",
+ "comment_url": "https://github.com/1btc-news/news-client/issues/33#issuecomment-4434671083",
+ "pr": "#44",
+ "pr_state": "CLOSED",
+ "amount_sats": null,
+ "btc_address": "bc1qmd4y3mjcewp54epetvtxzcy8vamgtf75r5nevr",
+ "txid": null,
+ "state": "acked",
+ "note": "Not counted as paid until bounty-poster approval and on-chain/payment proof are visible."
+ },
+ {
+ "author": "slashdevcorpse",
+ "created_at": "2026-05-12T22:31:22Z",
+ "comment_url": "https://github.com/1btc-news/news-client/issues/33#issuecomment-4435360703",
+ "pr": "#48",
+ "pr_state": "OPEN",
+ "amount_sats": null,
+ "btc_address": "bc1qzt33s8l22dgdfu8qdm3q5teu7529pl8tp5hkv8",
+ "txid": null,
+ "state": "requested",
+ "note": "Not counted as paid until bounty-poster approval and on-chain/payment proof are visible."
+ },
+ {
+ "author": "SimoneMariaRomeo",
+ "created_at": "2026-05-13T14:27:28Z",
+ "comment_url": "https://github.com/1btc-news/news-client/issues/33#issuecomment-4442026567",
+ "pr": "#47",
+ "pr_state": "MERGED",
+ "amount_sats": 15000,
+ "btc_address": "bc1q5z8wd9fzvzzvvtkpvevae7jjh9sxut8lnlzauv",
+ "txid": null,
+ "state": "requested",
+ "note": "Not counted as paid until bounty-poster approval and on-chain/payment proof are visible."
+ },
+ {
+ "author": "SimoneMariaRomeo",
+ "created_at": "2026-05-13T14:40:22Z",
+ "comment_url": "https://github.com/1btc-news/news-client/issues/33#issuecomment-4442162797",
+ "pr": "#51",
+ "pr_state": "OPEN",
+ "amount_sats": null,
+ "btc_address": "bc1q5z8wd9fzvzzvvtkpvevae7jjh9sxut8lnlzauv",
+ "txid": null,
+ "state": "requested",
+ "note": "Not counted as paid until bounty-poster approval and on-chain/payment proof are visible."
+ }
+ ],
+ "requested_sats": 30000,
+ "confirmed_paid_sats": 0,
+ "pending_requests": 6,
+ "paid_requests": 0,
+ "verifier": "Issue #33 comments plus on-chain BTC transaction proof for each address/txid."
+ },
"revenue_x402_sats": 100,
"revenue_x402_events": 1,
- "revenue_x402_last_7d_events": "unknown — revenue KV unavailable during May 11 refresh",
+ "revenue_x402_last_7d_events": "unknown",
"revenue_x402_recent": [
{
"ts": "2026-04-16T16:44:19.015Z",
@@ -45,31 +125,35 @@
"inscription_sales_sats": 0
},
"narrative_traction": {
- "issue_33_total_comments": 191,
- "issue_33_unique_participants": 22,
- "issue_33_iskander_comments": 57,
- "quantum_visualizer_merged_prs": 13,
- "quantum_visualizer_pr_contributors": 6,
+ "issue_33_total_comments": 212,
+ "issue_33_unique_participants": 27,
+ "issue_33_iskander_comments": 59,
+ "quantum_visualizer_merged_prs": 16,
+ "quantum_visualizer_pr_contributors": 9,
"pr_contributor_handles": [
+ "SimoneMariaRomeo",
+ "slashdevcorpse",
+ "mySebbe",
+ "Iskander-Agent",
+ "lekanbams",
"1feems",
"Dannye013",
- "Iskander-Agent",
"SlyHarp",
- "lekanbams",
"tearful-saw"
],
"dashboard_visits": "unknown — no analytics instrumented",
"x_engagement": "unknown — x-posting paused (credits depleted)"
},
"freshness": {
- "signals_fetched_at": "2026-05-11T04:12:04Z",
- "github_fetched_at": "2026-05-11T04:12:04Z",
- "revenue_kv_fetched_at": "not refreshed — missing /home/admin/.openclaw/.env Cloudflare token in this environment",
+ "signals_fetched_at": "2026-05-13T14:54:53.968Z",
+ "github_fetched_at": "2026-05-13T14:54:53.968Z",
+ "revenue_kv_fetched_at": "not refreshed - CLOUDFLARE_API_TOKEN not found in environment or .openclaw/.env",
"next_refresh_target": "weekly synthesis (Sundays)"
},
"notes": [
"Silence is not a data point. Unknown fields stay unknown until verified.",
"Regenerate with: node scripts/build-customer.mjs",
- "May 11 manual refresh updated public signal and GitHub metrics only; revenue ledger was left unchanged because Cloudflare KV credentials were unavailable."
+ "bounty_33_payout_ledger is parsed from issue #33 comments. Pending requests are not counted as paid.",
+ "Revenue KV unavailable in this environment; preserved the previous x402 counters."
]
}
diff --git a/public/index.html b/public/index.html
index 4d13a43..ce64b80 100644
--- a/public/index.html
+++ b/public/index.html
@@ -169,6 +169,27 @@
.freshness-chip span{font-family:'JetBrains Mono',monospace;color:var(--text-muted);font-size:0.68rem}
@media(max-width:760px){.freshness-body{grid-template-columns:1fr}.freshness-kpis{grid-template-columns:1fr 1fr}.freshness-item{grid-template-columns:1fr}}
+/* Payout ledger */
+.payout-section{padding:2.5rem 0;border-top:1px solid var(--border)}
+.payout-panel{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-elevated);overflow:hidden}
+.payout-head{padding:1.25rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;flex-wrap:wrap}
+.payout-sub{font-size:0.82rem;color:var(--text-secondary);margin-top:0.35rem;max-width:720px;line-height:1.6}
+.payout-body{padding:1.25rem;background:var(--bg)}
+.payout-kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--border);border-radius:var(--radius-sm);overflow:hidden;margin-bottom:1rem}
+.payout-kpi{background:var(--bg-elevated);padding:0.9rem;min-width:0}
+.payout-num{font-family:'JetBrains Mono',monospace;font-size:1.15rem;font-weight:800;letter-spacing:0}
+.payout-label{font-size:0.62rem;font-weight:700;letter-spacing:0.07em;text-transform:uppercase;color:var(--text-muted);margin-top:0.2rem}
+.payout-table-wrap{border:1px solid var(--border);border-radius:var(--radius-sm);overflow:auto;-webkit-overflow-scrolling:touch}
+.payout-table{min-width:760px;font-size:0.82rem}
+.payout-table td{background:var(--bg)}
+.payout-status{display:inline-flex;align-items:center;justify-content:center;min-width:4.5rem;padding:0.25rem 0.45rem;border:1px solid var(--border);border-radius:100px;font-size:0.68rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted);white-space:nowrap}
+.payout-status.status-paid{border-color:rgba(34,197,94,0.3);background:rgba(34,197,94,0.1);color:#22c55e}
+.payout-status.status-requested,.payout-status.status-acked{border-color:rgba(247,147,26,0.28);background:rgba(247,147,26,0.08);color:var(--accent)}
+.payout-link{font-size:0.75rem;color:var(--accent);text-decoration:none;font-weight:500;white-space:nowrap}
+.payout-link:hover{text-decoration:underline}
+.payout-mono{font-family:'JetBrains Mono',monospace;font-size:0.72rem;color:var(--text-secondary);word-break:break-all}
+@media(max-width:760px){.payout-kpis{grid-template-columns:1fr}.payout-body{padding:0.75rem}}
+
/* API section */
.api-section{padding:2rem 0;border-top:1px solid var(--border)}
.api-card{display:flex;align-items:center;justify-content:space-between;background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-sm);padding:1rem 1.25rem;flex-wrap:wrap;gap:0.75rem}
@@ -410,6 +431,40 @@
Source Freshness Audit
+
+
+
+
+
+
+
Payout Ledger
+
Customer world model view of bounty #33 payout requests and payment proof. Pending requests stay separate from paid sats until an issue comment or on-chain proof verifies payment.
+
+
+
+
+
+
+
+
+
+| Comment |
+PR |
+Amount |
+Status |
+Verifier |
+
+
+
+| Loading payout ledger... |
+
+
+
+
+
+
+
+
@@ -641,6 +696,55 @@
Ecosystem
renderFreshnessAudit();
+function formatSats(value){
+if(value==null||value===''||Number.isNaN(Number(value)))return '--';
+return Number(value).toLocaleString()+' sats';
+}
+
+function payoutStatusClass(value){
+return 'status-'+String(value||'tracked').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
+}
+
+function renderPayoutLedger(customer){
+const body=document.getElementById('payout-ledger-body');
+const kpis=document.getElementById('payout-kpis');
+const asof=document.getElementById('payout-asof');
+if(!body||!kpis||!asof)return;
+const ledger=customer&&customer.sats_flow&&customer.sats_flow.bounty_33_payout_ledger;
+if(!ledger||!Array.isArray(ledger.rows)){
+asof.textContent='Customer model unavailable';
+kpis.innerHTML='
';
+body.innerHTML='
| Payout ledger data is not available in /customer.json. |
';
+return;
+}
+asof.textContent=ledger.extracted_at?'Extracted '+String(ledger.extracted_at).slice(0,10):'Issue #33';
+kpis.innerHTML=[
+{num:formatSats(ledger.requested_sats),label:'requested'},
+{num:formatSats(ledger.confirmed_paid_sats),label:'confirmed paid'},
+{num:ledger.pending_requests||0,label:'pending rows'}
+].map(k=>`
${escapeHtml(k.num)}
${escapeHtml(k.label)}
`).join('');
+const rows=ledger.rows.slice().reverse();
+body.innerHTML=rows.length
+?rows.map(r=>{
+const verifier=r.txid
+?`
${escapeHtml(r.txid)}
`
+:r.btc_address?`
${escapeHtml(r.btc_address)}
`:'
awaiting proof';
+return `
+| ${escapeHtml(r.author||'comment')} ${escapeHtml((r.created_at||'').slice(0,10))} |
+${r.pr?escapeHtml(r.pr):'--'} ${escapeHtml(r.pr_state||'unknown')} |
+${escapeHtml(formatSats(r.amount_sats))} |
+${escapeHtml(r.state||'tracked')} |
+${verifier} |
+
`;
+}).join('')
+:'
| No payout request or payment proof comments found in issue #33. |
';
+}
+
+fetch('/customer.json')
+.then(r=>r.ok?r.json():null)
+.then(renderPayoutLedger)
+.catch(()=>renderPayoutLedger(null));
+
// Stats
const statsEl=document.getElementById('stats');
const scores=[
diff --git a/scripts/build-customer.mjs b/scripts/build-customer.mjs
index aa8f5c2..b457eaf 100755
--- a/scripts/build-customer.mjs
+++ b/scripts/build-customer.mjs
@@ -23,23 +23,134 @@ function gh(cmd) {
return execSync(cmd, { encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
}
+function readExistingCustomer() {
+ try {
+ return JSON.parse(fs.readFileSync(OUT, "utf8"));
+ } catch {
+ return null;
+ }
+}
+
function readEnv(key) {
- const envFile = `${process.env.HOME}/.openclaw/.env`;
- const line = fs.readFileSync(envFile, "utf8").split("\n").find((l) => l.startsWith(`${key}=`));
- if (!line) throw new Error(`${key} not in ${envFile}`);
- return line.slice(key.length + 1);
+ if (process.env[key]) return process.env[key];
+ const homes = [process.env.HOME, process.env.USERPROFILE].filter(Boolean);
+ for (const home of homes) {
+ const envFile = `${home}/.openclaw/.env`;
+ if (!fs.existsSync(envFile)) continue;
+ const line = fs.readFileSync(envFile, "utf8").split("\n").find((l) => l.startsWith(`${key}=`));
+ if (line) return line.slice(key.length + 1).trim();
+ }
+ throw new Error(`${key} not found in environment or .openclaw/.env`);
+}
+
+async function fetchRevenueLedger(existing) {
+ try {
+ const token = readEnv("CLOUDFLARE_API_TOKEN");
+ const url = `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${KV_NAMESPACE_ID}/values/ledger:events`;
+ const r = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
+ if (r.status === 404) return { events: [], totalSats: 0, totalEvents: 0, last7dEvents: 0, fetchedAt: new Date().toISOString(), note: null };
+ if (!r.ok) throw new Error(`KV fetch failed: ${r.status}`);
+ const text = await r.text();
+ const events = JSON.parse(text || "[]");
+ const last7dEvents = events.filter((e) => (e.ts || "").slice(0, 10) >= sevenDaysAgo).length;
+ return {
+ events,
+ totalSats: events.reduce((sum, e) => sum + (e.sats || 0), 0),
+ totalEvents: events.length,
+ last7dEvents,
+ fetchedAt: new Date().toISOString(),
+ note: null,
+ };
+ } catch (error) {
+ const sats = existing?.sats_flow?.revenue_x402_sats ?? "unknown";
+ const events = existing?.sats_flow?.revenue_x402_events ?? "unknown";
+ const last7dEvents = existing?.sats_flow?.revenue_x402_last_7d_events;
+ return {
+ events: existing?.sats_flow?.revenue_x402_recent || [],
+ totalSats: sats,
+ totalEvents: events,
+ last7dEvents: typeof last7dEvents === "number" ? last7dEvents : "unknown",
+ fetchedAt: `not refreshed - ${error.message}`,
+ note: "Revenue KV unavailable in this environment; preserved the previous x402 counters.",
+ };
+ }
+}
+
+function parseSats(body) {
+ const match = String(body || "").match(/([0-9][0-9,]*)\s*(?:sat|sats|satoshis)\b/i);
+ return match ? Number(match[1].replace(/,/g, "")) : null;
+}
+
+function parseBtcAddress(body) {
+ const match = String(body || "").match(/\bbc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{20,90}\b/i);
+ return match ? match[0] : null;
}
-async function fetchRevenueLedger() {
- const token = readEnv("CLOUDFLARE_API_TOKEN");
- const url = `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${KV_NAMESPACE_ID}/values/ledger:events`;
- const r = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
- if (r.status === 404) return [];
- if (!r.ok) throw new Error(`KV fetch failed: ${r.status}`);
- const text = await r.text();
- try { return JSON.parse(text); } catch { return []; }
+function parseTxid(body) {
+ const match = String(body || "").match(/\b[0-9a-f]{64}\b/i);
+ return match ? match[0] : null;
+}
+
+function parsePrNumber(body) {
+ const text = String(body || "");
+ const urlMatch = text.match(/quantum-visualizer\/pull\/(\d+)/i);
+ if (urlMatch) return Number(urlMatch[1]);
+ const prMatch = text.match(/\bPR\s*#(\d+)\b/i);
+ return prMatch ? Number(prMatch[1]) : null;
+}
+
+function inferPayoutState(body) {
+ const text = String(body || "").toLowerCase();
+ const negative = /not paid|no paid|no received transaction|pending|awaiting|zero transactions/.test(text);
+ if (!negative && /paid on-chain|payment sent|payout sent|txid|transaction id|on-chain proof/.test(text)) return "paid";
+ if (/(payment|payout) request ack/.test(text)) return "acked";
+ if (/payout request|payment request|requesting\s+\*{0,2}[0-9,]+\s*sats|payout route/.test(text)) return "requested";
+ return "tracked";
+}
+
+function makePayoutLedger({ comments, prsRaw }) {
+ const prsByNumber = new Map(prsRaw.map((p) => [Number(p.number), p]));
+ const rows = [];
+ for (const comment of comments) {
+ const body = comment.body || "";
+ const isPaymentComment = /(payout|payment)\s+(request|route|acked|sent|paid)|requesting\s+\*{0,2}[0-9,]+\s*sats|paid on-chain/i.test(body);
+ if (!isPaymentComment) continue;
+ if (!/(sats|bc1|quantum-visualizer\/pull|PR\s*#|txid)/i.test(body)) continue;
+ const prNumber = parsePrNumber(body);
+ const pr = prNumber ? prsByNumber.get(prNumber) : null;
+ const amountSats = parseSats(body);
+ const state = inferPayoutState(body);
+ rows.push({
+ author: comment.user?.login || "unknown",
+ created_at: comment.created_at,
+ comment_url: comment.html_url,
+ pr: prNumber ? `#${prNumber}` : null,
+ pr_state: pr?.state || "unknown",
+ amount_sats: amountSats,
+ btc_address: parseBtcAddress(body),
+ txid: parseTxid(body),
+ state,
+ note: state === "paid"
+ ? "Payment proof detected in issue #33 comment."
+ : "Not counted as paid until bounty-poster approval and on-chain/payment proof are visible.",
+ });
+ }
+
+ const requestedRows = rows.filter((r) => r.amount_sats && r.state !== "paid");
+ const paidRows = rows.filter((r) => r.amount_sats && r.state === "paid");
+ return {
+ source_url: "https://github.com/1btc-news/news-client/issues/33",
+ extracted_at: new Date().toISOString(),
+ rows,
+ requested_sats: requestedRows.reduce((sum, r) => sum + r.amount_sats, 0),
+ confirmed_paid_sats: paidRows.reduce((sum, r) => sum + r.amount_sats, 0),
+ pending_requests: rows.filter((r) => r.state !== "paid").length,
+ paid_requests: rows.filter((r) => r.state === "paid").length,
+ verifier: "Issue #33 comments plus on-chain BTC transaction proof for each address/txid.",
+ };
}
+const existingCustomer = readExistingCustomer();
const signalsRes = await fetchJSON("https://aibtc.news/api/signals?limit=500");
const allSignals = signalsRes.signals || signalsRes;
const quantum = allSignals.filter((s) => (s.beatSlug || "").includes("quantum"));
@@ -61,12 +172,11 @@ const prsRaw = JSON.parse(
const merged = prsRaw.filter((p) => p.state === "MERGED");
const pr_contributors = [...new Set(merged.map((p) => p.author.login))];
-const ledger = await fetchRevenueLedger();
-const revenueSats = ledger.reduce((sum, e) => sum + (e.sats || 0), 0);
-const eventsLast7d = ledger.filter((e) => (e.ts || "").slice(0, 10) >= sevenDaysAgo);
+const revenue = await fetchRevenueLedger(existingCustomer);
+const payoutLedger = makePayoutLedger({ comments, prsRaw });
const customer = {
- schema_version: 2,
+ schema_version: 3,
as_of: today,
quantum_beats: {
total: quantum.length,
@@ -81,11 +191,12 @@ const customer = {
note: "Original research bounty (Issue #30), on-chain proof",
},
bounty_33_pool_sats: 250000,
- bounty_33_paid_confirmed: "unknown — awaiting on-chain payout ledger in #33",
- revenue_x402_sats: revenueSats,
- revenue_x402_events: ledger.length,
- revenue_x402_last_7d_events: eventsLast7d.length,
- revenue_x402_recent: ledger.slice(-5),
+ bounty_33_paid_confirmed: payoutLedger.confirmed_paid_sats,
+ bounty_33_payout_ledger: payoutLedger,
+ revenue_x402_sats: revenue.totalSats,
+ revenue_x402_events: revenue.totalEvents,
+ revenue_x402_last_7d_events: revenue.last7dEvents,
+ revenue_x402_recent: revenue.events.slice(-5),
inscription_sales_sats: 0,
},
narrative_traction: {
@@ -101,14 +212,16 @@ const customer = {
freshness: {
signals_fetched_at: new Date().toISOString(),
github_fetched_at: new Date().toISOString(),
- revenue_kv_fetched_at: new Date().toISOString(),
+ revenue_kv_fetched_at: revenue.fetchedAt,
next_refresh_target: "weekly synthesis (Sundays)",
},
notes: [
"Silence is not a data point. Unknown fields stay unknown until verified.",
"Regenerate with: node scripts/build-customer.mjs",
+ "bounty_33_payout_ledger is parsed from issue #33 comments. Pending requests are not counted as paid.",
+ ...(revenue.note ? [revenue.note] : []),
],
};
fs.writeFileSync(OUT, JSON.stringify(customer, null, 2) + "\n");
-console.log(`wrote customer.json: ${quantum.length} beats, ${comments.length} comments, ${merged.length} merged PRs, ${ledger.length} x402 events (${revenueSats} sats)`);
+console.log(`wrote customer.json: ${quantum.length} beats, ${comments.length} comments, ${merged.length} merged PRs, ${revenue.totalEvents} x402 events (${revenue.totalSats} sats), ${payoutLedger.rows.length} payout rows`);
diff --git a/scripts/check-frontend.mjs b/scripts/check-frontend.mjs
index 9f407f3..1f67b72 100644
--- a/scripts/check-frontend.mjs
+++ b/scripts/check-frontend.mjs
@@ -15,11 +15,16 @@ for (const id of [
"freshness-body",
"freshness-updates",
"freshness-stale-list",
+ "payout-panel",
+ "payout-kpis",
+ "payout-ledger-body",
]) {
assert(html.includes(`id="${id}"`), `missing #${id}`);
}
assert(html.includes("function renderFreshnessAudit"), "missing renderFreshnessAudit()");
+assert(html.includes("function renderPayoutLedger"), "missing renderPayoutLedger()");
+assert(html.includes("fetch('/customer.json')"), "missing customer world model fetch");
const scriptMatch = html.match(/