openvibers/openvibe.codes Commits
Docs: /docs/billing renders the billing policy from OpenVibe.Billing's /policy.json (WS-K task 10): currencies, purchase tiers with the creator value and what OpenVibe keeps, tips, the subscription split and fee, cashout minimum and hold; cached 5 min, a problem (502) and no numbers when Billing does not answer; in the docs index and the sitemap
Changes
@@@ -14,6 +14,7 @@* helpers, from the installed openvibe-shared and openvibe-sdk, with a live sample* /docs/events event types from the service manifests* /docs/services the Network's registry with health as reported (never invented)++ * /docs/billing the billing policy as OpenVibe.Billing's /policy.json states it (never restated)* /docs/sdk[/:module] SDK reference from its .d.ts files* /docs/adr/:id ADRs as published in openvibe-contracts*/@@@ -52,6 +53,7 @@ function createDocsRoutes(ctx) {<li><a href="/docs/events"><strong>Events</strong></a><span>${docs.events.length} event types services declare they produce</span></li><li><a href="/docs/services"><strong>Services</strong></a><span>the registry as OpenVibe.Network reports it, with health</span></li><li><a href="/docs/tools"><strong>Tools API</strong></a><span>every OpenVibe tool you can call from code, from the live registry</span></li>++<li><a href="/docs/billing"><strong>Billing policy</strong></a><span>prices, the creator split, fees, holds and cashouts, live from OpenVibe.Billing</span></li><li><a href="/docs/sdk"><strong>SDK</strong></a><span>${docs.sdk.length} modules of openvibe-sdk from their type definitions</span></li><li><a href="/policy/rfc"><strong>Decisions</strong></a><span>${docs.adrs.length} architecture decision records</span></li></ul>@@@ -357,6 +359,54 @@ ${families.map((f) => html`<h2>${f}</h2>${table(['Tool', 'Runs as', 'Access', 'I});});++ // ── Billing policy (live from OpenVibe.Billing, WS-K task 10) ──++ // The numbers are Billing's /policy.json (read from the rates the ledger charges); nothing is++ // restated here. Billing not answering shows a problem, never remembered or invented numbers.++ const BILLING_URL = (process.env.OV_BILLING_INTERNAL_URL || 'http://127.0.0.1:4600').replace(/\/$/, '');++ let billingCache = { at: 0, policy: null };++ r.get('/billing', async (req, res) => {++ let policy = billingCache.policy; let problem = null;++ if (!policy || Date.now() - billingCache.at > 300_000) {++ try {++ const out = await fetch(`${BILLING_URL}/policy.json`, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(4000) });++ if (!out.ok) throw new Error(`Billing answered ${out.status}`);++ policy = await out.json();++ billingCache = { at: Date.now(), policy };++ } catch (err) { problem = { code: 'codes.billing_unavailable', detail: err.message }; }++ }++ const usd = (c) => `$${(c / 100).toFixed(2)}`;++ const n = (x) => Number(x).toLocaleString('en-US');++ const p = policy;++ send(res, problem && !p ? 502 : 200, {++ index: true, cache: 'public, max-age=300', viewer: req.viewer, config, path: req.originalUrl,++ title: 'Billing policy',++ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Billing' }],++ body: html`<h1>Billing policy</h1>++<p>How money moves on OpenVibe, as <a href="https://billing.openvibe.network/policy">billing.openvibe.network/policy</a> states it for everyone. The numbers below are Billing's <a href="https://billing.openvibe.network/policy.json"><code>/policy.json</code></a>, read from the rates the ledger charges, fetched at most every 5 minutes. Apps that take payments or show balances should link to that page rather than restate it.</p>++${problem && !p ? problemBox(problem, { title: 'The billing policy could not be read' }) : html`++<p>Ledger: <strong>${p.authority === 'billing' ? 'OpenVibe Billing' : 'openvibe.live'}</strong> (<code>authority: ${p.authority}</code>). Wording last changed ${p.wording_date}.</p>++<h2>Currencies</h2>++${table(['Currency', 'Bought with money', 'Withdrawable'], [++ ['Vibes', 'yes', `only Vibes received; ${usd(p.currencies.vibes.cash_value_per_100_cents)} per 100`],++ ['OpenCoins', 'no', 'never'],++ ['Channel points', 'no', 'never'],++])}++<h2>Buying Vibes</h2>++${table(['Vibes in one purchase', 'Price per 100', 'Creator value per 100', 'OpenVibe keeps'], p.purchase.tiers.map((t) => [++ `${n(t.from)}${t.to == null ? ' or more' : `–${n(t.to)}`}`, usd(t.price_per_100_cents), usd(t.creator_value_per_100_cents), `${t.openvibe_keeps_pct}%`,++]))}++<p>One purchase: ${n(p.purchase.min_vibes)} to ${n(p.purchase.max_vibes)} Vibes. Tips reach the creator in full (${p.tips.creator_receives_pct}%).</p>++<h2>Subscriptions and cashouts</h2>++${table(['Rule', 'Value'], [++ ['Subscription', `${usd(p.subscription.price_cents)} for ${n(p.subscription.period_days)} days`],++ ['Creator share (paid through OpenVibe’s PowerChat)', `${p.subscription.creator_share_pct}% (${usd(p.subscription.creator_share_cents)})`],++ ['Site routing fee, on top', `${p.subscription.site_route_fee_pct}% (${usd(p.subscription.site_route_fee_cents)})`],++ ['Minimum cashout', `${n(p.cashout.min_vibes)} Vibes (${usd(p.cashout.min_cents)})`],++ ['Cashout hold, then review', `${n(p.cashout.hold_days)} days`],++])}`}`,++ });++ });++// ── SDK ─────────────────────────────────────────────────r.get('/sdk', (req, res) => {page(req, res, {@@@ -271,7 +271,7 @@ ${signedIn ? releaseActions(req, rel) : ''}].join('\n'));});r.get('/sitemap.xml', (req, res) => {-- const urls = ['/', '/docs', '/docs/api', '/docs/updates', '/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/services', '/docs/tools', '/docs/sdk', '/oauth', '/tools/webhooks', '/manifests/validate',++ const urls = ['/', '/docs', '/docs/api', '/docs/updates', '/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/services', '/docs/billing', '/docs/tools', '/docs/sdk', '/oauth', '/tools/webhooks', '/manifests/validate','/policy', '/policy/rfc', '/policy/compatibility', '/policy/licensing', '/policy/transparency',...governance.filter((g) => !g.draft).map((g) => `/policy/${g.slug}`),...docs.contracts.map((c) => `/docs/contracts/${c.id}`), ...docs.capabilities.map((c) => `/docs/capabilities/${c.id}`),nnew file mode 100644@@@ -0,0 +1,56 @@++'use strict';++/**++ * /docs/billing (WS-K task 10) renders OpenVibe.Billing's /policy.json and nothing of its own: the++ * numbers on the page are the ones Billing answered, and Billing not answering is a 502 with a++ * problem box, never numbers from anywhere else.++ */++const assert = require('assert');++const http = require('http');++++const POLICY = {++ authority: 'live', wording_date: '2026-09-26',++ currencies: { vibes: { bought_with_money: true, cash_value_per_100_cents: 100, withdrawable: 'only Vibes you received' }, opencoins: { bought_with_money: false, withdrawable: false }, channel_points: { bought_with_money: false, withdrawable: false } },++ purchase: { min_vibes: 100, max_vibes: 10000000, tiers: [{ from: 100, to: 499, price_per_100_cents: 150, creator_value_per_100_cents: 100, openvibe_keeps_pct: 33.3 }, { from: 500, to: null, price_per_100_cents: 137, creator_value_per_100_cents: 100, openvibe_keeps_pct: 27 }] },++ tips: { creator_receives_pct: 100 },++ subscription: { price_cents: 499, period_days: 31, creator_share_pct: 70, creator_share_cents: 349, site_route_fee_pct: 10, site_route_fee_cents: 50 },++ cashout: { min_vibes: 500, min_cents: 500, hold_days: 14 },++};++++(async () => {++ let up = true;++ const billing = http.createServer((req, res) => {++ if (!up) { res.statusCode = 503; return res.end('{}'); }++ if (req.url !== '/policy.json') { res.statusCode = 404; return res.end(); }++ res.setHeader('Content-Type', 'application/json');++ res.end(JSON.stringify(POLICY));++ });++ await new Promise((r) => billing.listen(0, '127.0.0.1', r));++ process.env.OV_BILLING_INTERNAL_URL = `http://127.0.0.1:${billing.address().port}`;++ const { boot, check, done } = require('./helpers/boot');++ const t = await boot();++++ await check('the page shows what Billing answered', async () => {++ const r = await t.get('/docs/billing');++ assert.strictEqual(r.status, 200, r.text.slice(0, 300));++ for (const s of ['$1.37', '27%', '500 or more', '$4.99 for 31 days', '70% ($3.49)', '10% ($0.50)', '500 Vibes ($5.00)', '14 days', 'openvibe.live', 'billing.openvibe.network/policy']) {++ assert.ok(r.text.includes(s), `the page says ${s}`);++ }++ assert.ok((await t.get('/docs')).text.includes('href="/docs/billing"'), 'the docs index links it');++ });++++ await check('Billing down on a cold cache: a problem, no numbers', async () => {++ up = false;++ // A fresh process has no cache; drop this one's by booting a second app.++ for (const k of Object.keys(require.cache)) if (k.includes('/server/http/docs.js')) delete require.cache[k];++ const t2 = await boot();++ const r = await t2.get('/docs/billing');++ assert.strictEqual(r.status, 502);++ assert.ok(r.text.includes('The billing policy could not be read'));++ assert.ok(!r.text.includes('$4.99'), 'no numbers without Billing');++ await t2.close();++ });++++ await t.close();++ billing.close();++ done();++})().catch((err) => { console.error(err); process.exit(1); });
@@ -14,6 +14,7 @@
@@ -14,6 +14,7 @@
* helpers, from the installed openvibe-shared and openvibe-sdk, with a live sample
* helpers, from the installed openvibe-shared and openvibe-sdk, with a live sample
* /docs/events event types from the service manifests
* /docs/events event types from the service manifests
* /docs/services the Network's registry with health as reported (never invented)
* /docs/services the Network's registry with health as reported (never invented)
+ * /docs/billing the billing policy as OpenVibe.Billing's /policy.json states it (never restated)
* /docs/sdk[/:module] SDK reference from its .d.ts files
* /docs/sdk[/:module] SDK reference from its .d.ts files
* /docs/adr/:id ADRs as published in openvibe-contracts
* /docs/adr/:id ADRs as published in openvibe-contracts
*/
*/
@@ -52,6 +53,7 @@ function createDocsRoutes(ctx) {
@@ -52,6 +53,7 @@ function createDocsRoutes(ctx) {
<li><a href="/docs/events"><strong>Events</strong></a><span>${docs.events.length} event types services declare they produce</span></li>
<li><a href="/docs/events"><strong>Events</strong></a><span>${docs.events.length} event types services declare they produce</span></li>
<li><a href="/docs/services"><strong>Services</strong></a><span>the registry as OpenVibe.Network reports it, with health</span></li>
<li><a href="/docs/services"><strong>Services</strong></a><span>the registry as OpenVibe.Network reports it, with health</span></li>
<li><a href="/docs/tools"><strong>Tools API</strong></a><span>every OpenVibe tool you can call from code, from the live registry</span></li>
<li><a href="/docs/tools"><strong>Tools API</strong></a><span>every OpenVibe tool you can call from code, from the live registry</span></li>
+<li><a href="/docs/billing"><strong>Billing policy</strong></a><span>prices, the creator split, fees, holds and cashouts, live from OpenVibe.Billing</span></li>
<li><a href="/docs/sdk"><strong>SDK</strong></a><span>${docs.sdk.length} modules of openvibe-sdk from their type definitions</span></li>
<li><a href="/docs/sdk"><strong>SDK</strong></a><span>${docs.sdk.length} modules of openvibe-sdk from their type definitions</span></li>
<li><a href="/policy/rfc"><strong>Decisions</strong></a><span>${docs.adrs.length} architecture decision records</span></li>
<li><a href="/policy/rfc"><strong>Decisions</strong></a><span>${docs.adrs.length} architecture decision records</span></li>
</ul>
</ul>
@@ -357,6 +359,54 @@ ${families.map((f) => html`<h2>${f}</h2>${table(['Tool', 'Runs as', 'Access', 'I
@@ -357,6 +359,54 @@ ${families.map((f) => html`<h2>${f}</h2>${table(['Tool', 'Runs as', 'Access', 'I
});
});
});
});
+ // ── Billing policy (live from OpenVibe.Billing, WS-K task 10) ──
+ // The numbers are Billing's /policy.json (read from the rates the ledger charges); nothing is
+ // restated here. Billing not answering shows a problem, never remembered or invented numbers.
+ const BILLING_URL = (process.env.OV_BILLING_INTERNAL_URL || 'http://127.0.0.1:4600').replace(/\/$/, '');
+ let billingCache = { at: 0, policy: null };
+ r.get('/billing', async (req, res) => {
+ let policy = billingCache.policy; let problem = null;
+ if (!policy || Date.now() - billingCache.at > 300_000) {
+ try {
+ const out = await fetch(`${BILLING_URL}/policy.json`, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(4000) });
+ if (!out.ok) throw new Error(`Billing answered ${out.status}`);
+ policy = await out.json();
+ billingCache = { at: Date.now(), policy };
+ } catch (err) { problem = { code: 'codes.billing_unavailable', detail: err.message }; }
+ }
+ const usd = (c) => `$${(c / 100).toFixed(2)}`;
+ const n = (x) => Number(x).toLocaleString('en-US');
+ const p = policy;
+ send(res, problem && !p ? 502 : 200, {
+ index: true, cache: 'public, max-age=300', viewer: req.viewer, config, path: req.originalUrl,
+ title: 'Billing policy',
+ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Billing' }],
+ body: html`<h1>Billing policy</h1>
+<p>How money moves on OpenVibe, as <a href="https://billing.openvibe.network/policy">billing.openvibe.network/policy</a> states it for everyone. The numbers below are Billing's <a href="https://billing.openvibe.network/policy.json"><code>/policy.json</code></a>, read from the rates the ledger charges, fetched at most every 5 minutes. Apps that take payments or show balances should link to that page rather than restate it.</p>
+${problem && !p ? problemBox(problem, { title: 'The billing policy could not be read' }) : html`
+<p>Ledger: <strong>${p.authority === 'billing' ? 'OpenVibe Billing' : 'openvibe.live'}</strong> (<code>authority: ${p.authority}</code>). Wording last changed ${p.wording_date}.</p>
+<h2>Currencies</h2>
+${table(['Currency', 'Bought with money', 'Withdrawable'], [
+ ['Vibes', 'yes', `only Vibes received; ${usd(p.currencies.vibes.cash_value_per_100_cents)} per 100`],
+ ['OpenCoins', 'no', 'never'],
+ ['Channel points', 'no', 'never'],
+])}
+<h2>Buying Vibes</h2>
+${table(['Vibes in one purchase', 'Price per 100', 'Creator value per 100', 'OpenVibe keeps'], p.purchase.tiers.map((t) => [
+ `${n(t.from)}${t.to == null ? ' or more' : `–${n(t.to)}`}`, usd(t.price_per_100_cents), usd(t.creator_value_per_100_cents), `${t.openvibe_keeps_pct}%`,
+]))}
+<p>One purchase: ${n(p.purchase.min_vibes)} to ${n(p.purchase.max_vibes)} Vibes. Tips reach the creator in full (${p.tips.creator_receives_pct}%).</p>
+<h2>Subscriptions and cashouts</h2>
+${table(['Rule', 'Value'], [
+ ['Subscription', `${usd(p.subscription.price_cents)} for ${n(p.subscription.period_days)} days`],
+ ['Creator share (paid through OpenVibe’s PowerChat)', `${p.subscription.creator_share_pct}% (${usd(p.subscription.creator_share_cents)})`],
+ ['Site routing fee, on top', `${p.subscription.site_route_fee_pct}% (${usd(p.subscription.site_route_fee_cents)})`],
+ ['Minimum cashout', `${n(p.cashout.min_vibes)} Vibes (${usd(p.cashout.min_cents)})`],
+ ['Cashout hold, then review', `${n(p.cashout.hold_days)} days`],
+])}`}`,
+ });
+ });
+
// ── SDK ─────────────────────────────────────────────────
// ── SDK ─────────────────────────────────────────────────
r.get('/sdk', (req, res) => {
r.get('/sdk', (req, res) => {
page(req, res, {
page(req, res, {
@@ -271,7 +271,7 @@ ${signedIn ? releaseActions(req, rel) : ''}
@@ -271,7 +271,7 @@ ${signedIn ? releaseActions(req, rel) : ''}
].join('\n'));
].join('\n'));
});
});
r.get('/sitemap.xml', (req, res) => {
r.get('/sitemap.xml', (req, res) => {
- const urls = ['/', '/docs', '/docs/api', '/docs/updates', '/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/services', '/docs/tools', '/docs/sdk', '/oauth', '/tools/webhooks', '/manifests/validate',
+ const urls = ['/', '/docs', '/docs/api', '/docs/updates', '/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/services', '/docs/billing', '/docs/tools', '/docs/sdk', '/oauth', '/tools/webhooks', '/manifests/validate',
'/policy', '/policy/rfc', '/policy/compatibility', '/policy/licensing', '/policy/transparency',
'/policy', '/policy/rfc', '/policy/compatibility', '/policy/licensing', '/policy/transparency',
...governance.filter((g) => !g.draft).map((g) => `/policy/${g.slug}`),
...governance.filter((g) => !g.draft).map((g) => `/policy/${g.slug}`),
...docs.contracts.map((c) => `/docs/contracts/${c.id}`), ...docs.capabilities.map((c) => `/docs/capabilities/${c.id}`),
...docs.contracts.map((c) => `/docs/contracts/${c.id}`), ...docs.capabilities.map((c) => `/docs/capabilities/${c.id}`),
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+'use strict';
+/**
+ * /docs/billing (WS-K task 10) renders OpenVibe.Billing's /policy.json and nothing of its own: the
+ * numbers on the page are the ones Billing answered, and Billing not answering is a 502 with a
+ * problem box, never numbers from anywhere else.
+ */
+const assert = require('assert');
+const http = require('http');
+
+const POLICY = {
+ authority: 'live', wording_date: '2026-09-26',
+ currencies: { vibes: { bought_with_money: true, cash_value_per_100_cents: 100, withdrawable: 'only Vibes you received' }, opencoins: { bought_with_money: false, withdrawable: false }, channel_points: { bought_with_money: false, withdrawable: false } },
+ purchase: { min_vibes: 100, max_vibes: 10000000, tiers: [{ from: 100, to: 499, price_per_100_cents: 150, creator_value_per_100_cents: 100, openvibe_keeps_pct: 33.3 }, { from: 500, to: null, price_per_100_cents: 137, creator_value_per_100_cents: 100, openvibe_keeps_pct: 27 }] },
+ tips: { creator_receives_pct: 100 },
+ subscription: { price_cents: 499, period_days: 31, creator_share_pct: 70, creator_share_cents: 349, site_route_fee_pct: 10, site_route_fee_cents: 50 },
+ cashout: { min_vibes: 500, min_cents: 500, hold_days: 14 },
+};
+
+(async () => {
+ let up = true;
+ const billing = http.createServer((req, res) => {
+ if (!up) { res.statusCode = 503; return res.end('{}'); }
+ if (req.url !== '/policy.json') { res.statusCode = 404; return res.end(); }
+ res.setHeader('Content-Type', 'application/json');
+ res.end(JSON.stringify(POLICY));
+ });
+ await new Promise((r) => billing.listen(0, '127.0.0.1', r));
+ process.env.OV_BILLING_INTERNAL_URL = `http://127.0.0.1:${billing.address().port}`;
+ const { boot, check, done } = require('./helpers/boot');
+ const t = await boot();
+
+ await check('the page shows what Billing answered', async () => {
+ const r = await t.get('/docs/billing');
+ assert.strictEqual(r.status, 200, r.text.slice(0, 300));
+ for (const s of ['$1.37', '27%', '500 or more', '$4.99 for 31 days', '70% ($3.49)', '10% ($0.50)', '500 Vibes ($5.00)', '14 days', 'openvibe.live', 'billing.openvibe.network/policy']) {
+ assert.ok(r.text.includes(s), `the page says ${s}`);
+ }
+ assert.ok((await t.get('/docs')).text.includes('href="/docs/billing"'), 'the docs index links it');
+ });
+
+ await check('Billing down on a cold cache: a problem, no numbers', async () => {
+ up = false;
+ // A fresh process has no cache; drop this one's by booting a second app.
+ for (const k of Object.keys(require.cache)) if (k.includes('/server/http/docs.js')) delete require.cache[k];
+ const t2 = await boot();
+ const r = await t2.get('/docs/billing');
+ assert.strictEqual(r.status, 502);
+ assert.ok(r.text.includes('The billing policy could not be read'));
+ assert.ok(!r.text.includes('$4.99'), 'no numbers without Billing');
+ await t2.close();
+ });
+
+ await t.close();
+ billing.close();
+ done();
+})().catch((err) => { console.error(err); process.exit(1); });