web-agent-bridge 3.9.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web-agent-bridge",
3
- "version": "3.9.0",
3
+ "version": "3.9.1",
4
4
  "description": "Agent Transaction Bridge — the trust + transaction layer for agentic commerce. Signed intent contracts, idempotent transactions, Ed25519-verifiable receipts, explicit compensation. Plus the original WAB stack: sovereign browser, ShieldQR, SSL health, DNS discovery, agent mesh, and unified gateway for safe AI–website interaction.",
5
5
  "author": "Web Agent Bridge <dev@webagentbridge.com>",
6
6
  "main": "server/index.js",
package/public/atp.html CHANGED
@@ -166,6 +166,9 @@ console.log(verification.verification.ok); // true</code></pre>
166
166
 
167
167
  <h2>Reference</h2>
168
168
  <p>Full API in <a href="/docs.html">/docs.html</a> and machine-readable spec in <code>docs/SPEC.md</code>. Mount path: <code>/api/atp</code>. Source: <a href="https://github.com/abokenan444/web-agent-bridge">github.com/abokenan444/web-agent-bridge</a>.</p>
169
+
170
+ <h2>We run our own business on it</h2>
171
+ <p>WAB doesn't just publish this protocol &mdash; it bills its own customers with it. Every subscription processed through webagentbridge.com produces a publicly-verifiable Ed25519 receipt. Audit the books at <a href="/transparency.html"><b>/transparency.html</b></a>.</p>
169
172
  </main>
170
173
  </body>
171
174
  </html>
@@ -0,0 +1,285 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WAB Transparency — Live ATP Receipts for Every Subscription</title>
7
+ <meta name="description" content="WAB doesn't just build the trust layer for agentic commerce — it runs its own business on it. Every subscription processed through webagentbridge.com is an ATP transaction with a public Ed25519 receipt.">
8
+ <meta property="og:title" content="WAB Transparency — Live ATP Receipts">
9
+ <meta property="og:description" content="Every WAB subscription produces a publicly-verifiable Ed25519 receipt. Audit the books, byte by byte.">
10
+ <link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
11
+ <style>
12
+ :root {
13
+ --bg:#0b0d12; --bg2:#11141b; --fg:#e6e8ec; --muted:#8a93a6;
14
+ --accent:#7c5cff; --accent2:#22d3ee; --ok:#10b981; --bad:#ef4444;
15
+ --border:#1f2430;
16
+ }
17
+ * { box-sizing:border-box; }
18
+ body {
19
+ margin:0; font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
20
+ background: radial-gradient(1200px 600px at 20% -10%, rgba(124,92,255,.18), transparent 60%),
21
+ radial-gradient(900px 500px at 90% 10%, rgba(34,211,238,.12), transparent 55%),
22
+ var(--bg);
23
+ color:var(--fg); line-height:1.55; min-height:100vh;
24
+ }
25
+ a { color:var(--accent2); text-decoration:none; }
26
+ a:hover { text-decoration:underline; }
27
+ .container { max-width: 1100px; margin: 0 auto; padding: 0 24px; }
28
+ header.top {
29
+ padding: 22px 0; border-bottom:1px solid var(--border);
30
+ display:flex; align-items:center; justify-content:space-between;
31
+ }
32
+ .brand { font-weight:700; letter-spacing:.2px; }
33
+ .brand span { color: var(--accent); }
34
+ nav a { margin-left: 18px; color: var(--muted); font-size:14px; }
35
+ nav a:hover { color: var(--fg); }
36
+
37
+ .hero { padding: 70px 0 50px; text-align:center; }
38
+ .hero .eyebrow {
39
+ display:inline-block; padding:6px 14px; border-radius:999px;
40
+ background: rgba(124,92,255,.12); color:#c8bdff; font-size:12px;
41
+ letter-spacing:.18em; text-transform:uppercase; border:1px solid rgba(124,92,255,.35);
42
+ }
43
+ .hero h1 {
44
+ font-size: clamp(34px, 5vw, 56px); margin: 18px 0 14px; line-height:1.08;
45
+ background: linear-gradient(180deg, #fff 30%, #b8c0d4 100%);
46
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
47
+ font-weight: 800; letter-spacing:-.5px;
48
+ }
49
+ .hero p.sub { color: var(--muted); max-width: 780px; margin: 0 auto; font-size:18px; }
50
+ .hero p.tag { margin-top: 22px; font-size: 15px; color:#cdd5e3; }
51
+ .hero p.tag strong { color: var(--fg); }
52
+
53
+ .stats {
54
+ display:grid; grid-template-columns: repeat(4, 1fr); gap: 14px;
55
+ margin: 36px 0 12px;
56
+ }
57
+ .stat {
58
+ background: linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.01));
59
+ border:1px solid var(--border); border-radius:14px; padding:18px;
60
+ }
61
+ .stat .k { color: var(--muted); font-size:12px; letter-spacing:.08em; text-transform:uppercase; }
62
+ .stat .v { font-size: 26px; font-weight: 700; margin-top:6px; }
63
+ .stat .v small { color: var(--muted); font-size:13px; font-weight:500; }
64
+ @media (max-width: 720px) { .stats { grid-template-columns: repeat(2, 1fr); } }
65
+
66
+ section { padding: 30px 0; }
67
+ h2 { font-size: 24px; margin: 28px 0 14px; letter-spacing:-.3px; }
68
+
69
+ .feed {
70
+ background: var(--bg2); border: 1px solid var(--border); border-radius: 14px; overflow:hidden;
71
+ }
72
+ table { width:100%; border-collapse: collapse; font-size: 14px; }
73
+ th, td { padding: 12px 14px; text-align: left; border-bottom: 1px solid var(--border); }
74
+ th { background: rgba(255,255,255,.02); color: var(--muted); font-weight: 600;
75
+ font-size: 12px; letter-spacing:.08em; text-transform:uppercase; }
76
+ tr:last-child td { border-bottom: 0; }
77
+ tr:hover td { background: rgba(124,92,255,.04); }
78
+ .mono { font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12.5px; }
79
+ .receipt-id { color: var(--accent2); }
80
+ .tier { display:inline-block; padding:3px 8px; border-radius:6px; font-size:11px;
81
+ font-weight:700; text-transform:uppercase; letter-spacing:.06em; }
82
+ .tier.starter { background: rgba(34,211,238,.15); color:#7ee9ff; }
83
+ .tier.pro { background: rgba(124,92,255,.18); color:#c8bdff; }
84
+ .tier.business { background: rgba(16,185,129,.18); color:#7feac3; }
85
+ .tier.enterprise{ background: rgba(245,158,11,.18); color:#ffd58a; }
86
+ .badge-verify {
87
+ display:inline-flex; align-items:center; gap:6px;
88
+ background: rgba(16,185,129,.12); color: var(--ok); padding: 4px 10px;
89
+ border-radius:999px; font-size:12px; font-weight:600; border:1px solid rgba(16,185,129,.3);
90
+ cursor:pointer; transition: all .15s;
91
+ }
92
+ .badge-verify:hover { background: rgba(16,185,129,.22); }
93
+ .badge-verify.bad { background: rgba(239,68,68,.12); color:var(--bad); border-color: rgba(239,68,68,.3); }
94
+ .badge-verify.loading { opacity:.6; }
95
+
96
+ .empty { padding:38px 18px; text-align:center; color: var(--muted); }
97
+ .empty code { background: var(--bg); padding:2px 6px; border-radius:4px; color: var(--accent2); }
98
+
99
+ .how {
100
+ background: var(--bg2); border:1px solid var(--border); border-radius:14px;
101
+ padding: 22px 24px; margin: 24px 0;
102
+ }
103
+ .how ol { padding-left: 20px; margin: 8px 0 0; }
104
+ .how li { margin: 8px 0; color:#cdd5e3; }
105
+ .how code { background: var(--bg); padding:2px 6px; border-radius:4px;
106
+ color: var(--accent2); font-size: 12.5px; }
107
+
108
+ footer {
109
+ border-top:1px solid var(--border); margin-top: 60px; padding: 30px 0;
110
+ color: var(--muted); font-size: 13px; text-align:center;
111
+ }
112
+ footer a { color: var(--muted); margin: 0 10px; }
113
+ </style>
114
+ </head>
115
+ <body>
116
+ <header class="top">
117
+ <div class="container" style="display:flex;align-items:center;justify-content:space-between;width:100%;">
118
+ <div class="brand">Web Agent Bridge <span>·</span> Transparency</div>
119
+ <nav>
120
+ <a href="/">Home</a>
121
+ <a href="/atp.html">ATP Spec</a>
122
+ <a href="/docs.html">Docs</a>
123
+ <a href="/premium.html">Pricing</a>
124
+ </nav>
125
+ </div>
126
+ </header>
127
+
128
+ <main>
129
+ <section class="hero">
130
+ <div class="container">
131
+ <div class="eyebrow">Eat your own cooking</div>
132
+ <h1>Every subscription is a public, signed receipt.</h1>
133
+ <p class="sub">
134
+ WAB doesn't just build the trust layer for agentic commerce — it runs its own
135
+ business on it. Every dollar that flows into webagentbridge.com is itself an
136
+ ATP transaction with a publicly-verifiable Ed25519 receipt.
137
+ </p>
138
+ <p class="tag">
139
+ <strong>This page reads directly from the production ATP ledger.</strong>
140
+ Click <em>Verify</em> on any row to re-check the signature in your browser.
141
+ </p>
142
+
143
+ <div class="stats" id="stats">
144
+ <div class="stat"><div class="k">Receipts issued</div><div class="v" id="s-count">—</div></div>
145
+ <div class="stat"><div class="k">Total settled</div><div class="v" id="s-total">—</div></div>
146
+ <div class="stat"><div class="k">First receipt</div><div class="v" id="s-first">—</div></div>
147
+ <div class="stat"><div class="k">Latest receipt</div><div class="v" id="s-last">—</div></div>
148
+ </div>
149
+ </div>
150
+ </section>
151
+
152
+ <section>
153
+ <div class="container">
154
+ <h2>Live receipt feed</h2>
155
+ <div class="feed">
156
+ <table>
157
+ <thead>
158
+ <tr>
159
+ <th>Issued</th>
160
+ <th>Tier</th>
161
+ <th>Amount</th>
162
+ <th>Receipt ID</th>
163
+ <th>Key</th>
164
+ <th style="text-align:right;">Signature</th>
165
+ </tr>
166
+ </thead>
167
+ <tbody id="rows">
168
+ <tr><td colspan="6" class="empty">Loading public ATP ledger…</td></tr>
169
+ </tbody>
170
+ </table>
171
+ </div>
172
+
173
+ <div class="how">
174
+ <h2 style="margin-top:0;">How to audit this yourself</h2>
175
+ <ol>
176
+ <li>List receipts: <code>GET /api/atp/platform/receipts?limit=50</code></li>
177
+ <li>Fetch full signed body: <code>GET /api/atp/receipts/&lt;receipt_id&gt;</code></li>
178
+ <li>Verify Ed25519 signature: <code>POST /api/atp/receipts/verify</code> with <code>{"receipt_id":"…"}</code></li>
179
+ <li>Or verify offline: every receipt embeds its own public key under <code>signature.public_key</code>. The canonical body is the receipt JSON with the <code>signature</code> field removed.</li>
180
+ </ol>
181
+ </div>
182
+ </div>
183
+ </section>
184
+ </main>
185
+
186
+ <footer>
187
+ <div class="container">
188
+ <div>WAB · <a href="/atp.html">ATP Protocol</a> · <a href="/docs.html">Docs</a> · <a href="/privacy.html">Privacy</a> · <a href="/terms.html">Terms</a></div>
189
+ <div style="margin-top:8px;">Receipts shown on this page are real, generated automatically when payments settle on Stripe.</div>
190
+ </div>
191
+ </footer>
192
+
193
+ <script>
194
+ const $ = (id) => document.getElementById(id);
195
+
196
+ function fmtMoney(cents, currency) {
197
+ const v = (cents || 0) / 100;
198
+ try { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'USD' }).format(v); }
199
+ catch { return `${v.toFixed(2)} ${currency || ''}`; }
200
+ }
201
+ function fmtDate(s) {
202
+ if (!s) return '—';
203
+ const d = new Date(s.replace(' ', 'T') + (s.includes('Z') ? '' : 'Z'));
204
+ if (isNaN(+d)) return s;
205
+ return d.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
206
+ }
207
+ function shortId(id, n = 12) { return id ? id.slice(0, n) + '…' : '—'; }
208
+
209
+ async function loadStats() {
210
+ try {
211
+ const r = await fetch('/api/atp/platform/stats').then(r => r.json());
212
+ const s = r.data || {};
213
+ $('s-count').innerHTML = (s.receipts || 0).toLocaleString();
214
+ // total_cents is mixed currency; show approximate USD-equivalent string with note
215
+ $('s-total').innerHTML = `${fmtMoney(s.total_cents || 0, 'USD')} <small>gross</small>`;
216
+ $('s-first').innerHTML = s.first_at ? fmtDate(s.first_at) : '—';
217
+ $('s-last').innerHTML = s.last_at ? fmtDate(s.last_at) : '—';
218
+ } catch (e) {
219
+ $('s-count').textContent = 'n/a';
220
+ }
221
+ }
222
+
223
+ async function verifyReceipt(receiptId, btn) {
224
+ btn.classList.add('loading');
225
+ btn.textContent = 'verifying…';
226
+ try {
227
+ const r = await fetch('/api/atp/receipts/verify', {
228
+ method: 'POST',
229
+ headers: { 'Content-Type': 'application/json' },
230
+ body: JSON.stringify({ receipt_id: receiptId })
231
+ }).then(r => r.json());
232
+ btn.classList.remove('loading');
233
+ if (r.ok && r.verification && r.verification.ok) {
234
+ btn.classList.remove('bad');
235
+ btn.innerHTML = '✓ signature valid';
236
+ } else {
237
+ btn.classList.add('bad');
238
+ btn.textContent = '✗ ' + ((r.verification && r.verification.reason) || 'invalid');
239
+ }
240
+ } catch (e) {
241
+ btn.classList.remove('loading');
242
+ btn.classList.add('bad');
243
+ btn.textContent = '✗ network';
244
+ }
245
+ }
246
+
247
+ async function loadReceipts() {
248
+ try {
249
+ const r = await fetch('/api/atp/platform/receipts?limit=50').then(r => r.json());
250
+ const rows = (r.data || []);
251
+ const tb = $('rows');
252
+ if (!rows.length) {
253
+ tb.innerHTML = `<tr><td colspan="6" class="empty">
254
+ No platform receipts yet. Receipts appear automatically when Stripe settles a subscription.<br>
255
+ Try the protocol with your own intent at <code>/atp.html</code>.
256
+ </td></tr>`;
257
+ return;
258
+ }
259
+ tb.innerHTML = rows.map(row => `
260
+ <tr>
261
+ <td>${fmtDate(row.issued_at)}</td>
262
+ <td><span class="tier ${(row.tier || '').toLowerCase()}">${row.tier || '—'}</span></td>
263
+ <td class="mono">${fmtMoney(row.amount_cents, row.currency)}</td>
264
+ <td class="mono receipt-id" title="${row.receipt_id}">
265
+ <a href="/api/atp/receipts/${row.receipt_id}" target="_blank" rel="noopener">${shortId(row.receipt_id, 22)}</a>
266
+ </td>
267
+ <td class="mono">${shortId(row.key_id, 10)}</td>
268
+ <td style="text-align:right;">
269
+ <button class="badge-verify" data-rid="${row.receipt_id}">Verify</button>
270
+ </td>
271
+ </tr>
272
+ `).join('');
273
+ tb.querySelectorAll('.badge-verify').forEach(btn => {
274
+ btn.addEventListener('click', () => verifyReceipt(btn.dataset.rid, btn));
275
+ });
276
+ } catch (e) {
277
+ $('rows').innerHTML = `<tr><td colspan="6" class="empty">Failed to load: ${e.message}</td></tr>`;
278
+ }
279
+ }
280
+
281
+ loadStats();
282
+ loadReceipts();
283
+ </script>
284
+ </body>
285
+ </html>
@@ -230,4 +230,19 @@ router.post('/receipts/verify', publicReceiptLimiter, express.json({ limit: '256
230
230
 
231
231
  router.get('/health', (req, res) => res.json({ ok: true, service: 'atp', version: '1.0.0' }));
232
232
 
233
+ // ─── Public transparency feed ─────────────────────────────────────────────
234
+ // WAB dogfoods ATP: every subscription payment processed via webagentbridge.com
235
+ // produces a publicly-verifiable Ed25519 receipt. This feed lists them.
236
+ router.get('/platform/receipts', publicReceiptLimiter, (req, res) => {
237
+ const limit = Math.min(100, Math.max(1, parseInt(req.query.limit, 10) || 20));
238
+ const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
239
+ const items = transactions.listPlatformReceipts({ limit, offset });
240
+ res.json({ ok: true, data: items, limit, offset });
241
+ });
242
+
243
+ router.get('/platform/stats', publicReceiptLimiter, (req, res) => {
244
+ const stats = transactions.getPlatformStats();
245
+ res.json({ ok: true, data: stats });
246
+ });
247
+
233
248
  module.exports = router;
@@ -138,6 +138,25 @@ function handleWebhookEvent(event) {
138
138
  periodStart: new Date(invoice.period_start * 1000).toISOString(),
139
139
  periodEnd: new Date(invoice.period_end * 1000).toISOString()
140
140
  });
141
+ // ── WAB dogfooding: record this real money event as an ATP receipt ──
142
+ // Every dollar that flows into WAB is itself a publicly-verifiable
143
+ // Ed25519 receipt. Failure here MUST NOT block payment confirmation.
144
+ try {
145
+ const transactions = require('./transactions');
146
+ transactions.recordPlatformPayment({
147
+ userId: sub.user_id,
148
+ amountCents: invoice.amount_paid,
149
+ currency: (invoice.currency || 'USD').toUpperCase(),
150
+ tier: sub.tier,
151
+ externalRef: invoice.id || invoice.payment_intent,
152
+ description: `WAB ${sub.tier} subscription`,
153
+ periodStart: invoice.period_start ? new Date(invoice.period_start * 1000).toISOString() : null,
154
+ periodEnd: invoice.period_end ? new Date(invoice.period_end * 1000).toISOString() : null,
155
+ provider: 'stripe',
156
+ });
157
+ } catch (e) {
158
+ console.error('[atp] recordPlatformPayment failed (non-fatal):', e.message);
159
+ }
141
160
  }
142
161
  }
143
162
  break;
@@ -507,6 +507,154 @@ function expireOverdueIntents() {
507
507
  return r.changes;
508
508
  }
509
509
 
510
+ // ── Platform self-dogfooding ─────────────────────────────────────────────────
511
+
512
+ /**
513
+ * Record a real WAB platform money event (e.g. Stripe subscription payment)
514
+ * as a complete ATP cycle: intent → authorize → tx → step → settle → receipt.
515
+ *
516
+ * This is "eat-your-own-cooking": every dollar that flows into WAB is itself
517
+ * a publicly-verifiable Ed25519 receipt that any auditor can re-verify.
518
+ *
519
+ * Idempotent on (externalRef): replaying the same Stripe invoice id is a
520
+ * no-op and returns the existing receipt.
521
+ */
522
+ function recordPlatformPayment(params) {
523
+ const {
524
+ userId, amountCents, currency = 'USD', tier,
525
+ externalRef, description = null,
526
+ periodStart = null, periodEnd = null,
527
+ provider = 'stripe',
528
+ } = params;
529
+
530
+ if (!userId) throw badRequest('userId required');
531
+ if (!Number.isInteger(amountCents) || amountCents < 0) throw badRequest('amountCents must be non-negative integer');
532
+ if (!externalRef || typeof externalRef !== 'string') throw badRequest('externalRef required');
533
+ if (!tier || typeof tier !== 'string') throw badRequest('tier required');
534
+
535
+ // Idempotency: if a receipt already exists for this external ref, return it.
536
+ const prior = db.prepare(`
537
+ SELECT r.id AS rid
538
+ FROM atp_receipts r
539
+ JOIN atp_transactions t ON t.id = r.transaction_id
540
+ JOIN atp_intents i ON i.id = t.intent_id
541
+ WHERE json_extract(i.metadata, '$.platform') = 1
542
+ AND json_extract(i.metadata, '$.external_ref') = ?
543
+ LIMIT 1
544
+ `).get(externalRef);
545
+ if (prior) return getReceipt(prior.rid);
546
+
547
+ const cur = String(currency || 'USD').toUpperCase();
548
+ const meta = {
549
+ platform: true,
550
+ kind: 'wab_subscription',
551
+ tier,
552
+ provider,
553
+ external_ref: externalRef,
554
+ period_start: periodStart,
555
+ period_end: periodEnd,
556
+ };
557
+
558
+ // 1. Intent
559
+ const intent = createIntent({
560
+ userId,
561
+ purpose: `WAB platform subscription — ${tier}`,
562
+ scope: { actions: ['pay'] },
563
+ spendCapCents: amountCents,
564
+ spendCurrency: cur,
565
+ maxExecutions: 1,
566
+ ttlSeconds: 3600,
567
+ metadata: meta,
568
+ });
569
+ // 2. Authorize
570
+ authorizeIntent(intent.id, { userId });
571
+ // 3. Begin tx (idempotent on externalRef)
572
+ const tx = beginTransaction({
573
+ intentId: intent.id,
574
+ idempotencyKey: externalRef,
575
+ amountCents,
576
+ currency: cur,
577
+ summary: description || `Subscription payment for ${tier}`,
578
+ metadata: { external_ref: externalRef, provider },
579
+ });
580
+ // 4. Step (the actual payment evidence)
581
+ appendStep(tx.id, {
582
+ action: 'pay',
583
+ evidence: {
584
+ provider,
585
+ external_ref: externalRef,
586
+ amount_cents: amountCents,
587
+ currency: cur,
588
+ tier,
589
+ period_start: periodStart,
590
+ period_end: periodEnd,
591
+ },
592
+ });
593
+ // 5. Drive lifecycle to settled
594
+ transitionTransaction(tx.id, 'executing');
595
+ transitionTransaction(tx.id, 'executed');
596
+ transitionTransaction(tx.id, 'settled');
597
+ // 6. Issue receipt
598
+ return issueReceipt(tx.id);
599
+ }
600
+
601
+ /**
602
+ * List platform-issued receipts (the "transparency feed").
603
+ * Public-safe: filters to intents with metadata.platform=1 only.
604
+ */
605
+ function listPlatformReceipts({ limit = 20, offset = 0 } = {}) {
606
+ const lim = Math.max(1, Math.min(100, Number(limit) || 20));
607
+ const off = Math.max(0, Number(offset) || 0);
608
+ const rows = db.prepare(`
609
+ SELECT r.id AS receipt_id,
610
+ r.issued_at AS issued_at,
611
+ r.algorithm AS algorithm,
612
+ r.key_id AS key_id,
613
+ t.amount_cents AS amount_cents,
614
+ t.currency AS currency,
615
+ t.status AS tx_status,
616
+ json_extract(i.metadata, '$.tier') AS tier,
617
+ json_extract(i.metadata, '$.provider') AS provider,
618
+ json_extract(i.metadata, '$.period_start') AS period_start,
619
+ json_extract(i.metadata, '$.period_end') AS period_end
620
+ FROM atp_receipts r
621
+ JOIN atp_transactions t ON t.id = r.transaction_id
622
+ JOIN atp_intents i ON i.id = t.intent_id
623
+ WHERE json_extract(i.metadata, '$.platform') = 1
624
+ ORDER BY r.issued_at DESC
625
+ LIMIT ? OFFSET ?
626
+ `).all(lim, off);
627
+ return rows;
628
+ }
629
+
630
+ /**
631
+ * Aggregate stats for the public transparency page.
632
+ */
633
+ function getPlatformStats() {
634
+ const row = db.prepare(`
635
+ SELECT COUNT(*) AS receipts,
636
+ COALESCE(SUM(t.amount_cents), 0) AS total_cents,
637
+ MIN(r.issued_at) AS first_at,
638
+ MAX(r.issued_at) AS last_at
639
+ FROM atp_receipts r
640
+ JOIN atp_transactions t ON t.id = r.transaction_id
641
+ JOIN atp_intents i ON i.id = t.intent_id
642
+ WHERE json_extract(i.metadata, '$.platform') = 1
643
+ `).get();
644
+ const byTier = db.prepare(`
645
+ SELECT json_extract(i.metadata, '$.tier') AS tier,
646
+ COUNT(*) AS receipts,
647
+ COALESCE(SUM(t.amount_cents), 0) AS total_cents
648
+ FROM atp_receipts r
649
+ JOIN atp_transactions t ON t.id = r.transaction_id
650
+ JOIN atp_intents i ON i.id = t.intent_id
651
+ WHERE json_extract(i.metadata, '$.platform') = 1
652
+ GROUP BY tier
653
+ ORDER BY total_cents DESC
654
+ `).all();
655
+ return { ...row, by_tier: byTier };
656
+ }
657
+
510
658
  module.exports = {
511
659
  // intents
512
660
  createIntent, getIntent, listIntentsForUser, authorizeIntent, revokeIntent,
@@ -520,6 +668,8 @@ module.exports = {
520
668
  compensateTransaction,
521
669
  // maintenance
522
670
  expireOverdueIntents,
671
+ // platform dogfooding
672
+ recordPlatformPayment, listPlatformReceipts, getPlatformStats,
523
673
  // re-exports for tests
524
674
  _validateScope: validateScope,
525
675
  };