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.

+
+ +
+
+
+
+ + + + + + + + + + + + + +
CommentPRAmountStatusVerifier
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='
--
requested
--
paid
--
pending
'; +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(/