openvibers/openvibe.ai Commits

Publish weekly real-traffic share and attach provider fidelity to offers (B8/B5) (#421)

OP OpenVibers committed 3 days ago · cdf869c958 · 13 files, +626 −11

B8: aggregate each completed UTC week from Exchange receipts into a stored weekly snapshot and serve it at GET /api/v1/exchange/share. Cells below k=20 requests are withheld, runs with history off, failed, cached and synthetic runs are excluded, developer apps fold into one category, and no person, app or prompt is named. The last completed week is also published hourly by housekeeping, so it survives receipt retention.

B5 read-through: read OpenVibe.Bench's public fidelity score (fail-soft, five-minute cache) and put it on the exact provider offer in /exchange/catalog and /exchange/market; null when unmeasured.

Changes

ddiff --git a/docs/exchange-platform.md b/docs/exchange-platform.md
iindex 827a6d3..be055e0 100644
---- a/docs/exchange-platform.md
++++ b/docs/exchange-platform.md
@@@ -69,3 +69,18 @@ per minute. Every known metadata field carries its own source URL. Provider-page
facts take precedence over models.dev. Tests disable live refresh and inject
recorded responses. The market projects public fields explicitly; credentials,
account ids, internal provider metadata and price-change actors are never returned.
++
++## Fidelity and real-traffic share
++
++Each offer in the market feed and the catalog carries a `fidelity` field when OpenVibe.Bench has measured that exact
++endpoint on its fixed objective set: `{score, n, interval, compared_to, measured_at, prompt_set}`. AI reads Bench's
++public `GET /api/v1/fidelity` (fail-soft, five-minute cache) and never measures or invents the number itself; when
++Bench has no measurement, or is unreachable, the field is null and the page says "not measured", never zero.
++
++`GET /api/v1/exchange/share` publishes the week's real-traffic share by model and app category, from the Exchange
++receipts of a completed UTC week Monday to Monday. It is aggregate only: a cell needs at least `k` requests
++(`AI_TRAFFIC_SHARE_K`, floor 20) to be published; smaller cells are withheld and counted in `withheld`. Runs whose
++person turned `history` off are excluded, as are failed, cached and synthetic runs; no prompt, answer, person or
++developer app is named (every app folds into the category `apps`). The first read stores the week in
++`exchange_traffic_share` and later reads return the stored copy, so the number survives receipt retention. Tests
++replay receipts into a real test database; no live call is made. Migration 0063 is expand-only.
ddiff --git a/migrations/0063_traffic_share.sql b/migrations/0063_traffic_share.sql
nnew file mode 100644
iindex 0000000..9ad25cc
---- /dev/null
++++ b/migrations/0063_traffic_share.sql
@@@ -0,0 +1,24 @@
++-- phase: expand
++-- Real-traffic share (plan 2026-11-09, stage B8).
++--
++-- One row per completed UTC week: the share of requests and tokens each model and each app category
++-- carried, read from the Exchange receipts. Only cells with at least k requests are published; a cell
++-- below k is withheld and only counted in the withheld totals. Receipts are pruned with their runs, so
++-- the weekly row is the durable copy: it stays after the receipts it was computed from are gone.
++--
++-- payload is the exact public answer (aggregate counts and shares, never a person, an app's name or a
++-- prompt). recomputed_at is the last time the same week was recomputed (before the receipts aged out).
++-- The weekly aggregate scans the last seven days of receipts; that read gets its own index.
++CREATE INDEX IF NOT EXISTS ai_receipts_created_idx ON ai_receipts (created_at);
++
++CREATE TABLE IF NOT EXISTS exchange_traffic_share (
++ week_start date PRIMARY KEY, -- Monday 00:00 UTC
++ week_end date NOT NULL, -- the next Monday, exclusive
++ k integer NOT NULL CHECK (k >= 1), -- the minimum requests per published cell
++ payload jsonb NOT NULL,
++ cells integer NOT NULL DEFAULT 0, -- cells before the k rule
++ published_cells integer NOT NULL DEFAULT 0,
++ withheld_cells integer NOT NULL DEFAULT 0,
++ computed_at timestamptz NOT NULL DEFAULT now(),
++ recomputed_at timestamptz
++);
ddiff --git a/server/api/exchange.js b/server/api/exchange.js
iindex 9ac21ec..65f32bb 100644
---- a/server/api/exchange.js
++++ b/server/api/exchange.js
@@@ -35,8 +35,16 @@ function memo(fn, ttlMs, clock) {
};
}
--function exchangeRouter({ db, registry, pool, quotas = null, env = process.env, clock = { now: () => Date.now() }, config = null, presets, receipts, auth, log = console, ownEndpoints = null }) {
-- const catalog = createCatalog({ db, registry, pool, env, clock, config });
++function exchangeRouter({ db, registry, pool, quotas = null, env = process.env, clock = { now: () => Date.now() }, config = null, presets, receipts, auth, log = console, ownEndpoints = null, fetchImpl = globalThis.fetch, fidelity = null, share = null }) {
++ // Bench's provider-fidelity read (stage B5): optional and fail-soft. Off in tests, where no Bench runs.
++ const fidelitySource = fidelity || require('../exchange/fidelity').createFidelitySource({
++ url: config && config.bench ? config.bench.url : null, fetchImpl, clock, log,
++ timeoutMs: 2500, enabled: !config || config.nodeEnv !== 'test',
++ });
++ const catalog = createCatalog({ db, registry, pool, env, clock, config, fidelity: fidelitySource });
++ const trafficShare = share || require('../exchange/share').createShare({
++ db, clock, log, k: config && config.trafficShare ? config.trafficShare.k : 20,
++ });
const usage = createUsage({ db, clock });
// The price book's log: what a price was, what it became and when it was read.
const priceStore = require('../pricebook/store').createPriceStore({ db, clock });
@@@ -48,7 +56,7 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,
return hits.get(key)();
};
const all = () => allFor(null);
-- const market = require('../exchange/market').createMarket({ db, registry, catalog: all, config, clock });
++ const market = require('../exchange/market').createMarket({ db, registry, catalog: all, config, clock, fidelity: fidelitySource });
const usageByWindow = new Map();
const r = express.Router();
@@@ -244,6 +252,20 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,
} catch (err) { return next(err); }
});
++ // Stage B8: the share of requests and tokens each model and each app category carried in a completed
++ // week. Aggregated only: cells below k requests are withheld, opt-outs are excluded, and no person, app
++ // or prompt is ever named.
++ r.get('/api/v1/exchange/share', publicCache(300), async (req, res, next) => {
++ try {
++ const week = req.query.week == null || req.query.week === '' ? null : String(req.query.week);
++ const out = await trafficShare.read({ week });
++ return res.json(out);
++ } catch (err) {
++ if (err && err.name === 'ShareError') return http.sendProblem(res, err.status, err.code, { detail: err.detail, ctx: req.ov });
++ return next(err);
++ }
++ });
++
r.get('/api/v1/exchange/usage', publicCache(60), async (req, res, next) => {
try {
const days = Math.max(1, Math.min(366, Number.parseInt(req.query.days, 10) || 30));
ddiff --git a/server/app.js b/server/app.js
iindex a3fee3e..46194e6 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -22,7 +22,7 @@ const pkg = require('../package.json');
const { adminRoutes } = require('openvibe-shared/config');
const ownerInputs = require('./owner-inputs');
--function createApp({ choices = null, searchFetch = null, config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null, preferences = null, grants = null, vault = null, settings = null, benchEvents = null, runBudgets = null, memory = null, memoryImport = null, memoryServices = null, memorySharing = null, community = null, endpoints = null, webhooks = null, presets = null, receipts = null, balance = null, creditGrants = null, sessionCookies = null, gpus = null, gpuMarket = null, abuse = null, priceBook = null, ownEndpoints = null, refusal = null, batches = null }) {
++function createApp({ choices = null, searchFetch = null, config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null, preferences = null, grants = null, vault = null, settings = null, benchEvents = null, runBudgets = null, memory = null, memoryImport = null, memoryServices = null, memorySharing = null, community = null, endpoints = null, webhooks = null, presets = null, receipts = null, balance = null, creditGrants = null, sessionCookies = null, gpus = null, gpuMarket = null, abuse = null, priceBook = null, ownEndpoints = null, refusal = null, batches = null, share = null }) {
const app = express();
// /.well-known/agent.json (openvibe-shared/agent-layer, SEO-2): what this site serves, for agents.
require('openvibe-shared/agent-layer').mount(app, { descriptor: require('./agent-descriptor') });
@@@ -109,7 +109,7 @@ function createApp({ choices = null, searchFetch = null, config, db, registry, p
app.use(exchangePlatform.router);
// OpenVibe.Exchange's read API (server/api/exchange.js): the public catalog and aggregate usage. Public on
// purpose — the site and anyone comparing prices reads the same numbers the router ranks on.
-- app.use(require('./api/exchange').exchangeRouter({ config, db, registry, pool, quotas, env, clock, presets, receipts, auth, log, ownEndpoints }));
++ app.use(require('./api/exchange').exchangeRouter({ config, db, registry, pool, quotas, env, clock, presets, receipts, auth, log, ownEndpoints, fetchImpl, share }));
// A person's own endpoint (the Exchange remake, product line 6): the private rent's person API and its
// OpenAI-compatible proxy. Only the owner's own Network user token reaches it; a service token never does.
app.use((req,_res,next) => { if (/^\/api\/v1\/exchange\/deployments(?:\/|$)/.test(req.path)) req.url = req.url.replace('/api/v1/exchange/deployments','/api/v1/own-endpoints'); next(); });
ddiff --git a/server/config.js b/server/config.js
iindex b21d9de..2d96444 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -102,6 +102,11 @@ function load(env = process.env) {
url: trimUrl(env.AI_BENCH_URL || env.OV_BENCH_INTERNAL_URL || env.OV_BENCH_URL || env.BENCH_URL || 'http://127.0.0.1:5230'),
timeoutMs: int(env.AI_BENCH_TIMEOUT_MS, 15000),
},
++ // Real-traffic share by model and app category (stage B8). The k rule is a floor: a cell below it
++ // is never published, whatever the environment says.
++ trafficShare: {
++ k: Math.max(20, int(env.AI_TRAFFIC_SHARE_K, 20)),
++ },
// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner role).
db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },
ddiff --git a/server/exchange/catalog.js b/server/exchange/catalog.js
iindex 895b1e6..b2c42a9 100644
---- a/server/exchange/catalog.js
++++ b/server/exchange/catalog.js
@@@ -116,7 +116,7 @@ const laterOf = (a, b) => {
return x > y ? x : y;
};
--function createCatalog({ db, registry, pool, env = process.env, clock = { now: () => Date.now() }, config = null }) {
++function createCatalog({ db, registry, pool, env = process.env, clock = { now: () => Date.now() }, config = null, fidelity = null }) {
// The free-tier admission rules are the router's (server/free-tier.js): the catalog marks an offer with what
// the free allowance and the anonymous Try it may actually use. config.freeUse is fixed at boot, like the
// provider keys and upstreams the rest of this read shows.
@@@ -146,6 +146,7 @@ function createCatalog({ db, registry, pool, env = process.env, clock = { now: (
const now = clock.now();
const providers = await registry.listProviders();
const byKey = new Map(providers.map((p) => [p.key, p]));
++ const fidelityMap = fidelity ? await fidelity.read() : new Map();
// The router's breaker state per provider (server/providers/index.js): an offer whose provider answered
// "insufficient balance" is exhausted until a call succeeds again, and the catalog says so rather than
// promising free capacity that cannot answer.
@@@ -229,6 +230,9 @@ function createCatalog({ db, registry, pool, env = process.env, clock = { now: (
refusal_rate: refusal && refusal.refusal_rate != null ? Number(refusal.refusal_rate) : null,
refusal_measured_on: refusal ? (String(refusal.measured_on || '').slice(0, 10) || null) : null,
refusal_prompt_set: refusal ? (refusal.prompt_set || null) : null,
++ // Bench's measured provider fidelity for this exact endpoint: score, n, interval, the model
++ // it was compared on and when. Null when nobody measured it (missing is not zero).
++ fidelity: fidelityMap.get(`${String(up.key).toLowerCase()}\0${String(row.model_key).toLowerCase()}`) || null,
updated_at: row.updated_at || provider.updated_at || null,
};
if (!groups.has(slug)) {
ddiff --git a/server/exchange/fidelity.js b/server/exchange/fidelity.js
nnew file mode 100644
iindex 0000000..f442465
---- /dev/null
++++ b/server/exchange/fidelity.js
@@@ -0,0 +1,80 @@
++'use strict';
++
++/**
++ * Provider fidelity on an Exchange offer (stage B5).
++ *
++ * OpenVibe.Bench measures the same fixed objective set through every provider that serves an open-weight
++ * model and publishes the latest score at GET /api/v1/fidelity. This is a read-through of that public
++ * answer, keyed by the provider and exact endpoint: AI never measures fidelity itself and never invents
++ * a score. Bench being down (or not configured) leaves the field null, which the catalog renders as "not
++ * measured", never as zero.
++ *
++ * The read is cached in this process for five minutes; a failed read keeps the last good answer for the
++ * same window, and a first failure is retried after a minute rather than on every page view.
++ */
++const CACHE_MS = 5 * 60 * 1000;
++const FAIL_MS = 60 * 1000;
++
++const keyOf = (provider, model) => `${String(provider || '').toLowerCase()}\0${String(model || '').toLowerCase()}`;
++const num = (v) => (v == null || v === '' || !Number.isFinite(Number(v)) ? null : Number(v));
++
++function createFidelitySource({ url = null, fetchImpl = globalThis.fetch, clock = { now: () => Date.now() }, log = console, timeoutMs = 2500, enabled = true } = {}) {
++ const base = String(url || '').replace(/\/+$/, '');
++ const live = enabled && Boolean(base) && typeof fetchImpl === 'function';
++ let hit = null;
++ let pending = null;
++
++ async function load() {
++ if (!live) return new Map();
++ let body;
++ try {
++ const res = await fetchImpl(`${base}/api/v1/fidelity`, {
++ headers: { Accept: 'application/json' },
++ signal: AbortSignal.timeout(Math.max(250, Math.min(5000, Number(timeoutMs) || 2500))),
++ });
++ if (!res.ok) throw new Error(`Bench answered ${res.status}`);
++ body = await res.json();
++ } catch (err) {
++ log.warn('[exchange] fidelity read failed: ' + String((err && err.message) || err).slice(0, 200));
++ return null;
++ }
++ const map = new Map();
++ for (const offer of (Array.isArray(body && body.offers) ? body.offers : [])) {
++ const provider = String((offer && offer.provider) || '').trim();
++ const model = String((offer && (offer.provider_model || offer.model)) || '').trim();
++ const score = num(offer && offer.score);
++ const n = num(offer && offer.n);
++ if (!provider || !model || score == null || n == null || n <= 0) continue;
++ const interval = offer.interval && num(offer.interval.lower) != null && num(offer.interval.upper) != null
++ ? { lower: num(offer.interval.lower), upper: num(offer.interval.upper) }
++ : null;
++ map.set(keyOf(provider, model), {
++ score,
++ n,
++ interval,
++ compared_to: (offer && (offer.compared_to || offer.model)) || null,
++ measured_at: (offer && offer.measured_at) || null,
++ prompt_set: (offer && offer.prompt_set) || (body && body.objective_set) || null,
++ });
++ }
++ return map;
++ }
++
++ /** The fidelity map for this snapshot. Never throws: the market/catalog answer without the field. */
++ async function read() {
++ if (!live) return new Map();
++ if (hit && clock.now() - hit.at < (hit.ok ? CACHE_MS : FAIL_MS)) return hit.value;
++ if (pending) return pending;
++ pending = load()
++ .then((map) => {
++ hit = { at: clock.now(), value: map || hit?.value || new Map(), ok: Boolean(map) };
++ return hit.value;
++ })
++ .finally(() => { pending = null; });
++ return pending;
++ }
++
++ return { read, enabled: live, url: base || null, keyOf };
++}
++
++module.exports = { createFidelitySource, keyOf, CACHE_MS, FAIL_MS };
ddiff --git a/server/exchange/market.js b/server/exchange/market.js
iindex 1270d2b..4280d28 100644
---- a/server/exchange/market.js
++++ b/server/exchange/market.js
@@@ -8,7 +8,7 @@ const recordKey = (provider, model) => `${provider}\0${String(model).toLowerCase
const num = (v) => v == null || v === '' || !Number.isFinite(Number(v)) || Number(v) < 0 ? null : Number(v);
const webUrl = (v) => typeof v === 'string' && /^https:\/\//.test(v) ? v : null;
--function buildOffers({ providers = [], rows = [], changes = [], catalogue = {}, metadata = {}, observations = [], presets = PROVIDERS }) {
++function buildOffers({ providers = [], rows = [], changes = [], catalogue = {}, metadata = {}, observations = [], presets = PROVIDERS, fidelity = null }) {
const publicKeys = new Set([...presets.map(p => p.key), ...SOURCES.filter(s => s.role === 'own').map(s => s.provider)]);
const byProvider = new Map(providers.filter(p => publicKeys.has(p.key)).map((p) => [p.key, p]));
const seeds = new Map();
@@@ -58,6 +58,9 @@ function buildOffers({ providers = [], rows = [], changes = [], catalogue = {},
routed: routed.some(o => o.provider === p.key && String(o.model).toLowerCase() === String(row.model_key).toLowerCase()),
uncensored: seed?.m.uncensored === true || presets.find(preset => preset.key === p.key)?.uncensored === true,
provider_access: p.provider_access || 'UNVERIFIED', field_sources: {},
++ // The measured provider fidelity from Bench, when Bench has measured this exact endpoint;
++ // absent (null) when nobody measured it, never a zero.
++ fidelity: fidelity && typeof fidelity.get === 'function' ? (fidelity.get(recordKey(p.key, row.model_key)) || null) : null,
};
// Pending price changes are the latest observed own-page price, even before the router
// approves charging it. Never publish the change log's actor, note or account data.
@@@ -107,20 +110,21 @@ function pageMarket(snapshot, query = {}) {
offers: offers.slice(offset, offset + limit), next_cursor: offset + limit < offers.length ? String(offset + limit) : null };
}
--function createMarket({ db, registry, catalog, config, clock = { now: Date.now }, metadata = null }) {
++function createMarket({ db, registry, catalog, config, clock = { now: Date.now }, metadata = null, fidelity = null }) {
const source = metadata || createModelMetadata({ db, now: () => clock.now(), enabled: config?.nodeEnv !== 'test' && process.env.NODE_ENV !== 'test' });
let hit = null; let pending = null;
async function snapshot() {
if (hit && clock.now() < hit.until) return hit.value;
if (!pending) pending = (async () => {
-- const [providers, rows, changes, catalogue, meta, observations] = await Promise.all([
++ const [providers, rows, changes, catalogue, meta, observations, fidelityMap] = await Promise.all([
registry.listProviders(), registry.listModels(),
db.prepare('SELECT provider_key, model_key, field, new_price, source, checked_at FROM price_changes ORDER BY checked_at DESC').all(),
catalog(), source.read(),
db.prepare('SELECT provider_key, model_key, price, source, checked_at FROM exchange_market_prices').all(),
++ fidelity ? fidelity.read() : Promise.resolve(new Map()),
]);
const value = { generated_at: new Date(clock.now()).toISOString(), metadata: { source: URL, checked_at: meta.checked_at, stale: meta.stale },
-- offers: buildOffers({ providers, rows, changes, catalogue, metadata: meta, observations }) };
++ offers: buildOffers({ providers, rows, changes, catalogue, metadata: meta, observations, fidelity: fidelityMap }) };
hit = { value, until: clock.now() + TTL_MS }; return value;
})().finally(() => { pending = null; });
return pending;
ddiff --git a/server/exchange/share.js b/server/exchange/share.js
nnew file mode 100644
iindex 0000000..c791e8b
---- /dev/null
++++ b/server/exchange/share.js
@@@ -0,0 +1,239 @@
++'use strict';
++
++/**
++ * Real-traffic share (plan 2026-11-09, stage B8).
++ *
++ * Which models real requests and tokens actually carried, per model and per app category, from the
++ * Exchange receipts. It is an aggregate or it is nothing: no prompt, no answer, no person, no app's
++ * name. Every cell needs at least k requests before it is published; a smaller cell is withheld and
++ * counted only in the withheld total, so a category with one user cannot be read back from the shares.
++ *
++ * The week is a completed UTC week (Monday 00:00 to the next Monday). The answer is stored once per
++ * week: receipts are pruned with their runs, and the stored row is the durable public record. The
++ * last completed week is computed on first read; a past week is only recomputed while its receipts are
++ * still in the retention window (never a silent empty week after they aged out).
++ *
++ * "Category" is the first-party service whose run answered (a product), or `apps` for every developer
++ * app together. Named developer apps would be per-app data, so they are never named; the network's
++ * own moderation requests are not Exchange traffic and are not counted.
++ */
++const DAY_MS = 86400000;
++const WEEK_MS = 7 * DAY_MS;
++const K_DEFAULT = 20;
++const RETENTION_DAYS = 30;
++const SCHEMA = 'exchange.traffic-share@1';
++
++class ShareError extends Error {
++ constructor(status, code, detail) {
++ super(detail);
++ this.name = 'ShareError';
++ this.status = status;
++ this.code = code;
++ this.detail = detail;
++ }
++}
++
++const round = (v) => Math.round(Number(v) * 1e6) / 1e6;
++const int = (v) => Math.max(0, Math.round(Number(v) || 0));
++const isoDay = (ms) => new Date(ms).toISOString().slice(0, 10);
++const dayString = (v) => (v instanceof Date ? isoDay(v.getTime()) : String(v || '').slice(0, 10));
++
++/** The Monday 00:00 UTC of the week a moment is in. */
++function mondayOf(ms) {
++ const d = new Date(ms);
++ const offset = (d.getUTCDay() + 6) % 7;
++ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - offset);
++}
++
++/** The last completed week: this week's Monday minus seven days. */
++function lastCompleteWeek(ms) {
++ return mondayOf(ms) - WEEK_MS;
++}
++
++function parseWeek(day) {
++ const s = String(day || '');
++ if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) throw new ShareError(422, 'traffic_share.week_invalid', 'week must be a YYYY-MM-DD Monday');
++ const ms = Date.parse(`${s}T00:00:00.000Z`);
++ if (!Number.isFinite(ms) || new Date(ms).getUTCDay() !== 1) throw new ShareError(422, 'traffic_share.week_invalid', 'week must be a Monday 00:00 UTC');
++ return ms;
++}
++
++/** A first-party service keeps its product name; every developer app folds into one public category. */
++function categoryOf(row) {
++ if (row.requester_type === 'app') return 'apps';
++ if (row.requester_type === 'service' && /^[a-z0-9][a-z0-9._-]{0,63}$/i.test(String(row.requester_id || ''))) return String(row.requester_id);
++ return 'other';
++}
++
++const shareOf = (n, total) => (total > 0 ? round(n / total) : null);
++
++function aggregate(cells, keyFields, keyOf) {
++ const map = new Map();
++ for (const cell of cells) {
++ const key = keyOf(cell);
++ const row = map.get(key) || { ...Object.fromEntries(keyFields.map((f) => [f, cell[f]])), requests: 0, tokens_in: 0, tokens_out: 0 };
++ row.requests += cell.requests;
++ row.tokens_in += cell.tokens_in;
++ row.tokens_out += cell.tokens_out;
++ map.set(key, row);
++ }
++ return [...map.values()];
++}
++
++/**
++ * Read the receipts of one completed week and publish what meets the k rule. Pure over the rows passed
++ * to it, so a test can replay a small fixture without a database or a paid call.
++ */
++function aggregateWeek({ rows = [], weekStart, k = K_DEFAULT, generatedAt = new Date().toISOString() } = {}) {
++ const start = typeof weekStart === 'number' ? weekStart : parseWeek(weekStart);
++ const cells = new Map();
++ let allRequests = 0;
++ let allTokens = 0;
++ for (const row of rows) {
++ const provider = String(row.provider || '').trim();
++ const model = String(row.model || '').trim();
++ if (!provider || !model) continue;
++ const category = categoryOf(row);
++ const key = `${category}\0${provider}\0${model}`;
++ const cell = cells.get(key) || { category, provider, model, requests: 0, tokens_in: 0, tokens_out: 0 };
++ cell.requests += int(row.requests);
++ cell.tokens_in += int(row.tokens_in);
++ cell.tokens_out += int(row.tokens_out);
++ cells.set(key, cell);
++ allRequests += int(row.requests);
++ allTokens += int(row.tokens_in) + int(row.tokens_out);
++ }
++ const all = [...cells.values()];
++ const published = all.filter((c) => c.requests >= Math.max(1, Number(k) || K_DEFAULT));
++ const withheld = all.filter((c) => c.requests < Math.max(1, Number(k) || K_DEFAULT));
++ const requests = published.reduce((n, c) => n + c.requests, 0);
++ const tokens = published.reduce((n, c) => n + c.tokens_in + c.tokens_out, 0);
++ const tokensIn = published.reduce((n, c) => n + c.tokens_in, 0);
++ for (const cell of published) cell.tokens = cell.tokens_in + cell.tokens_out;
++ const offers = aggregate(published, ['provider', 'model'], (c) => `${c.provider}\0${c.model}`)
++ .map((r) => ({
++ provider: r.provider, model: r.model, requests: r.requests,
++ tokens_in: r.tokens_in, tokens_out: r.tokens_out, tokens: r.tokens_in + r.tokens_out,
++ request_share: shareOf(r.requests, requests), token_share: shareOf(r.tokens_in + r.tokens_out, tokens),
++ }))
++ .sort((a, b) => b.requests - a.requests || b.tokens - a.tokens || a.model.localeCompare(b.model) || a.provider.localeCompare(b.provider));
++ const models = aggregate(published, ['model'], (c) => c.model)
++ .map((r) => {
++ const providers = new Set(published.filter((c) => c.model === r.model).map((c) => c.provider)).size;
++ return { model: r.model, providers, requests: r.requests, tokens_in: r.tokens_in, tokens_out: r.tokens_out,
++ tokens: r.tokens_in + r.tokens_out, request_share: shareOf(r.requests, requests), token_share: shareOf(r.tokens_in + r.tokens_out, tokens) };
++ })
++ .sort((a, b) => b.requests - a.requests || b.tokens - a.tokens || a.model.localeCompare(b.model));
++ const categories = aggregate(published, ['category'], (c) => c.category)
++ .map((r) => ({ category: r.category, requests: r.requests, tokens_in: r.tokens_in, tokens_out: r.tokens_out,
++ tokens: r.tokens_in + r.tokens_out, request_share: shareOf(r.requests, requests), token_share: shareOf(r.tokens_in + r.tokens_out, tokens) }))
++ .sort((a, b) => b.requests - a.requests || b.tokens - a.tokens || a.category.localeCompare(b.category));
++ const withheldRequests = withheld.reduce((n, c) => n + c.requests, 0);
++ const withheldTokens = withheld.reduce((n, c) => n + c.tokens_in + c.tokens_out, 0);
++ return {
++ $schema: SCHEMA,
++ week_start: isoDay(start),
++ week_end: isoDay(start + WEEK_MS),
++ generated_at: generatedAt,
++ k: Math.max(1, Number(k) || K_DEFAULT),
++ totals: { requests, tokens_in: tokensIn, tokens_out: tokens - tokensIn, tokens },
++ offers,
++ models,
++ categories,
++ withheld: { cells: withheld.length, requests: withheldRequests, tokens: withheldTokens, share_of_requests: shareOf(withheldRequests, allRequests) },
++ source: 'Exchange receipts: succeeded, non-synthetic requests that a provider answered.',
++ note: published.length
++ ? 'Only cells with at least k requests are published. Every developer app is the single category apps.'
++ : 'No model and category reached k requests this week, so nothing is published.',
++ };
++}
++
++function createShare({ db, clock = { now: () => Date.now() }, k = K_DEFAULT, log = console } = {}) {
++ const minimum = Math.max(1, Number(k) || K_DEFAULT);
++ const byCell = db.prepare(`SELECT r.requester_type, r.requester_id,
++ COALESCE(r.body -> 'offer' ->> 'provider', '') AS provider,
++ COALESCE(r.body -> 'offer' ->> 'model', '') AS model,
++ COUNT(*)::bigint AS requests,
++ COALESCE(SUM(NULLIF(r.body -> 'usage' ->> 'tokens_in', '')::bigint), 0) AS tokens_in,
++ COALESCE(SUM(NULLIF(r.body -> 'usage' ->> 'tokens_out', '')::bigint), 0) AS tokens_out
++ FROM ai_receipts r
++ JOIN runs u ON u.id = r.run_id
++ WHERE r.created_at >= ?::timestamptz AND r.created_at < ?::timestamptz
++ AND r.requester_type IN ('service', 'app')
++ AND u.status = 'succeeded' AND u.synthetic = 0 AND u.provider_key IS NOT NULL
++ AND COALESCE(u.options::jsonb ->> 'history', 'true') <> 'false'
++ AND NULLIF(r.body -> 'offer' ->> 'provider', '') IS NOT NULL
++ AND NULLIF(r.body -> 'offer' ->> 'model', '') IS NOT NULL
++ GROUP BY 1, 2, 3, 4`);
++ const insert = db.prepare(`INSERT INTO exchange_traffic_share
++ (week_start, week_end, k, payload, cells, published_cells, withheld_cells, computed_at)
++ VALUES (?::date, ?::date, ?, ?::jsonb, ?, ?, ?, now())
++ ON CONFLICT (week_start) DO UPDATE SET
++ k = EXCLUDED.k, payload = EXCLUDED.payload, cells = EXCLUDED.cells,
++ published_cells = EXCLUDED.published_cells, withheld_cells = EXCLUDED.withheld_cells,
++ recomputed_at = now()`);
++ const stored = db.prepare('SELECT week_start, payload, computed_at, recomputed_at FROM exchange_traffic_share WHERE week_start = ?::date');
++ const latest = db.prepare('SELECT week_start, payload, computed_at, recomputed_at FROM exchange_traffic_share ORDER BY week_start DESC LIMIT 1');
++
++ const decode = (row) => ({
++ ...(row.payload && typeof row.payload === 'object' ? row.payload : JSON.parse(row.payload || '{}')),
++ week_start: dayString(row.week_start),
++ computed_at: row.computed_at instanceof Date ? row.computed_at.toISOString() : String(row.computed_at || ''),
++ recomputed_at: row.recomputed_at ? (row.recomputed_at instanceof Date ? row.recomputed_at.toISOString() : String(row.recomputed_at)) : null,
++ stored: true,
++ });
++
++ async function compute(weekStart) {
++ const start = isoDay(weekStart);
++ const end = isoDay(weekStart + WEEK_MS);
++ const rows = await byCell.all(`${start}T00:00:00.000Z`, `${end}T00:00:00.000Z`);
++ return aggregateWeek({ rows, weekStart, k: minimum, generatedAt: new Date(clock.now()).toISOString() });
++ }
++
++ async function store(payload) {
++ const withheld = payload.withheld ? payload.withheld.cells : 0;
++ const published = payload.offers.length;
++ const allCells = published + withheld;
++ await insert.run(payload.week_start, payload.week_end, payload.k, JSON.stringify(payload), allCells, published, withheld);
++ }
++
++ async function storedWeek(weekStart) {
++ const row = await stored.get(isoDay(weekStart));
++ return row ? decode(row) : null;
++ }
++
++ /**
++ * The public answer. With no week: the last completed week (computed and stored on first read, so the
++ * publication exists without a timer). With a week: that week, while its receipts are still kept.
++ */
++ async function read({ week = null } = {}) {
++ const now = clock.now();
++ const current = mondayOf(now);
++ if (week) {
++ const start = parseWeek(week);
++ if (start >= current) throw new ShareError(422, 'traffic_share.week_future', 'that week has not finished yet');
++ const known = await storedWeek(start);
++ if (known) return known;
++ if (now - start > RETENTION_DAYS * DAY_MS) throw new ShareError(404, 'traffic_share.expired', 'the receipts for that week are no longer kept; only the weeks already stored are available');
++ const payload = await compute(start);
++ await store(payload);
++ return { ...payload, computed_at: new Date(clock.now()).toISOString(), recomputed_at: null, stored: true };
++ }
++ const target = lastCompleteWeek(now);
++ const row = await latest.get();
++ if (row && dayString(row.week_start) === isoDay(target)) return decode(row);
++ const payload = await compute(target);
++ await store(payload);
++ return { ...payload, computed_at: new Date(clock.now()).toISOString(), recomputed_at: null, stored: true };
++ }
++
++ /** The hourly housekeeping pass: publish the last completed week even when nobody has read it. */
++ async function ensure() {
++ const out = await read();
++ return { week_start: out.week_start, stored: Boolean(out.stored) };
++ }
++
++ return { read, ensure, compute, store, aggregateWeek };
++}
++
++module.exports = { createShare, aggregateWeek, categoryOf, mondayOf, lastCompleteWeek, parseWeek, ShareError, SCHEMA, K_DEFAULT, RETENTION_DAYS };
ddiff --git a/server/index.js b/server/index.js
iindex 9f59ccc..00b2d6d 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -122,6 +122,9 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
// The Exchange's routing presets and the receipts of the answered requests (server/exchange/).
const presets = require('./exchange/presets').createPresets({ db, clock, log });
const receipts = require('./exchange/receipts').createReceipts({ db, clock, log });
++ // The weekly real-traffic share (stage B8): the public read stores the week; the housekeeping pass
++ // publishes it even when nobody reads it, before the receipts it came from are pruned.
++ const share = require('./exchange/share').createShare({ db, clock, log, k: config.trafficShare.k });
const choices = require('./choices').createChoices({ db, registry, preferences, credentials, machines, adultStatus, ratings: require('./choices/ratings').createRatings({ fetchImpl, clock }), clock, catalog: require('./exchange/catalog').createCatalog({ db, registry, pool, config, env, clock }) });
await choices.seed();
const projectSecrets = require('./project-secrets').createProjectSecrets({ config, env, fetchImpl });
@@@ -208,7 +211,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
clientSecret: env.OV_OAUTH_CLIENT_SECRET, fetch: fetchImpl,
}) : null;
const benchEvents = createBenchEvents({ bench, accountData, accountSend, config, log, fetchImpl, clientSecret: env.OV_OAUTH_CLIENT_SECRET });
-- const app = createApp({ choices, searchFetch, config, db, registry, pool, quotas, cache, runs, auth, keys, env, clock, fetchImpl, log, credentials, preferences, grants, vault, settings, benchEvents, runBudgets, memory, memoryImport, memoryServices, memorySharing, community, endpoints, webhooks, presets, receipts, balance, creditGrants, sessionCookies, gpus, abuse, priceBook, ownEndpoints, refusal, batches, gpuMarket });
++ const app = createApp({ choices, searchFetch, config, db, registry, pool, quotas, cache, runs, auth, keys, env, clock, fetchImpl, log, credentials, preferences, grants, vault, settings, benchEvents, runBudgets, memory, memoryImport, memoryServices, memorySharing, community, endpoints, webhooks, presets, receipts, balance, creditGrants, sessionCookies, gpus, abuse, priceBook, ownEndpoints, refusal, batches, gpuMarket, share });
let jwks = null;
let keyLoaded = Promise.resolve(null);
if (!keys.pinned && config.nodeEnv !== 'test') {
@@@ -228,6 +231,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
if (pruned || expired || lapsed) log.log(`[ai] retention: ${pruned} old run(s), ${expired} expired cache entr${expired === 1 ? 'y' : 'ies'}, ${lapsed} unsent usage reading(s)`);
if (Object.values(abusePruned).some((n) => n)) log.log(`[ai] abuse retention: ${Object.entries(abusePruned).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(', ')}`);
} catch (err) { log.error(`[ai] retention failed: ${err.message}`); }
++ try { await share.ensure(); } catch (err) { log.warn(`[ai] traffic share failed: ${err.message}`); }
}, 60 * 60 * 1000);
housekeeping.unref?.();
ddiff --git a/server/openapi.js b/server/openapi.js
iindex 433a8bc..dd780aa 100644
---- a/server/openapi.js
++++ b/server/openapi.js
@@@ -454,6 +454,7 @@ const exchangeOffer = {
refusal_rate: nullable('number', { minimum: 0, maximum: 1, description: 'The share of the published probe set this offer refused (0.75 is three probes out of four). null when nobody measured this offer, never 0.' }),
refusal_measured_on: nullable('string', { description: 'The day the refusal rate was measured' }),
refusal_prompt_set: nullable('string', { description: 'The version of the published probe set the rate was measured with' }),
++ fidelity: nullable('object', { description: 'Provider fidelity measured by OpenVibe.Bench: the same fixed objective set run through every provider that serves this open-weight model. score is the pass rate, n the answered items, interval its 95% Wilson band. Null when Bench has not measured this exact endpoint.', properties: { score: { type: 'number', minimum: 0, maximum: 1 }, n: integer({ minimum: 0 }), interval: { type: ['object', 'null'], properties: { lower: nullable('number'), upper: nullable('number') } }, compared_to: nullable('string'), measured_at: nullable('string'), prompt_set: nullable('string') } }),
updated_at: nullable('string'),
},
};
@@@ -491,6 +492,7 @@ route('get', '/exchange/market', {
maker: nullable('string'), context: nullable('number'), max_output: nullable('number'),
open_weights: nullable('boolean'), licence: nullable('string'), release_date: nullable('string'), knowledge_cutoff: nullable('string'),
modalities: { type: ['object', 'null'], properties: { input: arrayOf(str()), output: arrayOf(str()) } },
++ fidelity: nullable('object', { properties: { score: nullable('number'), n: integer(), interval: { type: ['object', 'null'], properties: { lower: nullable('number'), upper: nullable('number') } }, compared_to: nullable('string'), measured_at: nullable('string'), prompt_set: nullable('string') } }),
field_sources: { type: 'object', additionalProperties: str({ format: 'uri' }) },
} }),
},
@@@ -626,6 +628,21 @@ route('get', '/exchange/usage', {
} }) },
});
++route('get', '/exchange/share', {
++ operationId: 'exchangeTrafficShare', summary: 'Real-traffic share by model and app category, published weekly', tags: ['exchange'], security: [], servers: [{ url: 'https://api.openvibe.exchange' }],
++ description: 'Public and aggregate only: never a prompt, an answer, a person or an app\'s name. Read from the Exchange receipts of a completed UTC week (succeeded, non-synthetic requests that a provider answered; runs whose person turned history off are excluded). Every cell needs at least k requests before it is published; smaller cells are withheld and counted only in withheld. Developer apps fold into one category, apps, so no app is named. The weekly answer is stored once and kept after the receipts it came from are pruned.',
++ parameters: [query('week', str(), 'A Monday, YYYY-MM-DD. Default: the last completed week')],
++ responses: { 200: json('The share', { type: 'object', required: ['$schema', 'week_start', 'week_end', 'k', 'totals', 'offers', 'models', 'categories', 'withheld'], properties: {
++ $schema: { const: 'exchange.traffic-share@1' }, week_start: str({ format: 'date' }), week_end: str({ format: 'date' }), generated_at: str({ format: 'date-time' }),
++ k: integer({ minimum: 20 }), source: str(), note: str(),
++ totals: { type: 'object', properties: { requests: integer(), tokens_in: integer(), tokens_out: integer(), tokens: integer() } },
++ offers: arrayOf({ type: 'object', properties: { provider: str(), model: str(), requests: integer(), tokens_in: integer(), tokens_out: integer(), tokens: integer(), request_share: nullable('number'), token_share: nullable('number') } }),
++ models: arrayOf({ type: 'object', properties: { model: str(), providers: integer(), requests: integer(), tokens: integer(), request_share: nullable('number'), token_share: nullable('number') } }),
++ categories: arrayOf({ type: 'object', properties: { category: str(), requests: integer(), tokens: integer(), request_share: nullable('number'), token_share: nullable('number') } }),
++ withheld: { type: 'object', properties: { cells: integer(), requests: integer(), tokens: integer(), share_of_requests: nullable('number') } },
++ } }), ...errors([404, 422]) },
++});
++
route('get', '/exchange/price-changes', {
operationId: 'exchangePriceChanges', summary: 'What a price was and what it became', tags: ['exchange'], security: [],
description: "Public. The price changelog: every change the weekly price book made, with the old price, the new price, the provider's own listing the price was read from and the day it was read. A change that waits for the owner's approval is not a price yet and is not here.",
ddiff --git a/test/exchange-fidelity.test.js b/test/exchange-fidelity.test.js
nnew file mode 100644
iindex 0000000..8158428
---- /dev/null
++++ b/test/exchange-fidelity.test.js
@@@ -0,0 +1,76 @@
++'use strict';
++
++// Stage B5, AI's side: Bench's public fidelity score is read through once, cached, and put on the exact
++// provider offer in the market feed. Bench down or unconfigured leaves the field null (not measured), never
++// a zero. The test fetches a fixture; no Bench and no provider is called.
++const assert = require('assert');
++const { createFidelitySource, keyOf } = require('../server/exchange/fidelity');
++const { buildOffers } = require('../server/exchange/market');
++
++const BENCH = {
++ $schema: 'exchange.fidelity@1',
++ objective_set: 'fidelity-2026-11-09.v1',
++ items: 10,
++ offers: [
++ { model: 'acme/alpha-1', provider: 'acme', provider_model: 'ACME/Alpha-1', score: 0.9, n: 10, passed: 9, interval: { lower: 0.596, upper: 0.982 }, compared_to: 'acme/alpha-1', prompt_set: 'fidelity-2026-11-09.v1', measured_at: '2026-11-09T08:00:00.000Z', measured_on: '2026-11-09' },
++ { model: 'acme/alpha-1', provider: 'beta', provider_model: 'beta/alpha-1', score: null, n: 0, interval: { lower: null, upper: null }, compared_to: 'acme/alpha-1', measured_at: '2026-11-09T08:00:00.000Z' },
++ ],
++};
++
++const presets = [{
++ key: 'acme', display_name: 'Acme', models: [{
++ model_key: 'acme/alpha-1', display_name: 'Alpha 1', type: 'chat',
++ cost: { in: 1, out: 2, cached: null }, source: 'https://acme.test/prices', checked: '2026-11-01',
++ }],
++}];
++
++(async () => {
++ let time = Date.parse('2026-11-09T09:00:00Z');
++ let calls = 0;
++ let fail = false;
++ const source = createFidelitySource({
++ url: 'https://bench.openvibe.services/',
++ fetchImpl: async (url) => {
++ calls++;
++ assert.strictEqual(url, 'https://bench.openvibe.services/api/v1/fidelity');
++ if (fail) throw new Error('offline');
++ return new Response(JSON.stringify(BENCH), { status: 200, headers: { 'content-type': 'application/json' } });
++ },
++ clock: { now: () => time },
++ log: { warn() {} },
++ });
++
++ const first = await source.read();
++ const score = first.get(keyOf('acme', 'ACME/Alpha-1'));
++ assert(score, 'the exact endpoint is keyed case-insensitively');
++ assert.strictEqual(score.score, 0.9);
++ assert.strictEqual(score.n, 10);
++ assert.deepStrictEqual(score.interval, { lower: 0.596, upper: 0.982 });
++ assert.strictEqual(score.compared_to, 'acme/alpha-1');
++ assert(!first.has(keyOf('beta', 'beta/alpha-1')), 'a row with no score is not published as a score');
++
++ await source.read();
++ assert.strictEqual(calls, 1, 'the read is cached');
++ time += 6 * 60 * 1000;
++ await source.read();
++ assert.strictEqual(calls, 2, 'the cache expires after five minutes');
++ fail = true;
++ time += 6 * 60 * 1000;
++ const afterFailure = await source.read();
++ assert.strictEqual(afterFailure.get(keyOf('acme', 'acme/alpha-1')).score, 0.9, 'a failed refresh keeps the last good answer');
++ assert.strictEqual(calls, 3);
++
++ const disabled = createFidelitySource({ url: 'https://bench.openvibe.services', fetchImpl: async () => { throw new Error('must not be called'); }, enabled: false });
++ assert.strictEqual((await disabled.read()).size, 0);
++
++ const offers = buildOffers({ presets, fidelity: first });
++ const offer = offers.find((o) => o.provider === 'acme' && o.provider_model === 'acme/alpha-1');
++ assert(offer, 'the preset offer is listed');
++ assert.strictEqual(offer.fidelity.score, 0.9);
++ assert.strictEqual(offer.fidelity.compared_to, 'acme/alpha-1');
++ assert.strictEqual(offer.fidelity.n, 10);
++ const unmeasured = buildOffers({ presets });
++ assert.strictEqual(unmeasured[0].fidelity, null, 'no measurement is null, never zero');
++
++ console.log('exchange fidelity: Bench score read once, attached to the exact offer, null when unmeasured');
++})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/test/exchange-traffic-share.test.js b/test/exchange-traffic-share.test.js
nnew file mode 100644
iindex 0000000..19d41e4
---- /dev/null
++++ b/test/exchange-traffic-share.test.js
@@@ -0,0 +1,125 @@
++'use strict';
++
++// Stage B8: the weekly real-traffic share from Exchange receipts. Aggregate only, k >= 20 per cell,
++// opt-outs respected, stored once per completed week. The tests replay a small week into a real
++// database; no test calls a provider.
++const assert = require('assert');
++const { boot, request, suite } = require('./helpers');
++const { aggregateWeek, mondayOf, lastCompleteWeek, categoryOf } = require('../server/exchange/share');
++
++const DAY_MS = 86400000;
++const t = suite('exchange-traffic-share');
++let h;
++
++let seq = 0;
++const uid = (p) => `${p}_${Date.now()}_${seq++}`;
++const iso = (ms) => new Date(ms).toISOString();
++
++async function insertRun({ id, type, requester, provider, model, created, history = true, status = 'succeeded', synthetic = 0 }) {
++ await h.db.prepare(`INSERT INTO runs (id, workflow_key, workflow_version, status, requester_type, requester_id, input, input_hash,
++ provider_key, model_key, synthetic, options, created_at, started_at, finished_at)
++ VALUES (?, 'ai.chat', 1, ?, ?, ?, '{}', 'input-hash', ?, ?, ?, ?, ?, ?, ?)`)
++ .run(id, status, type, requester, provider, model, synthetic, JSON.stringify({ history }), created, created, created);
++}
++async function insertReceipt({ runId, type, requester, provider, model, created, tokensIn = 100, tokensOut = 10 }) {
++ const body = {
++ id: `rcpt_${runId}`,
++ run_id: runId,
++ created_at: created,
++ offer: { provider, model },
++ usage: { tokens_in: tokensIn, tokens_out: tokensOut },
++ };
++ await h.db.prepare(`INSERT INTO ai_receipts (id, run_id, requester_type, requester_id, body, created_at)
++ VALUES (?, ?, ?, ?, ?::jsonb, ?)`)
++ .run(body.id, runId, type, requester, JSON.stringify(body), created);
++}
++
++let weekStart;
++async function addTraffic({ type, requester, provider, model, count, history = true, status = 'succeeded', tokensIn = 100, tokensOut = 10 }) {
++ for (let i = 0; i < count; i++) {
++ const id = uid('run');
++ const created = iso(weekStart + (i % 4) * 6 * 3600000);
++ await insertRun({ id, type, requester, provider, model, created, history, status });
++ await insertReceipt({ runId: id, type, requester, provider, model, created, tokensIn, tokensOut });
++ }
++}
++
++t.test('aggregateWeek publishes only cells with k requests and folds apps into one category', () => {
++ const rows = [
++ { requester_type: 'service', requester_id: 'live', provider: 'deepseek', model: 'deepseek-chat', requests: 25, tokens_in: 2500, tokens_out: 250 },
++ { requester_type: 'service', requester_id: 'live', provider: 'qwen', model: 'qwen2.5', requests: 5, tokens_in: 500, tokens_out: 50 },
++ { requester_type: 'app', requester_id: 'prj_PRIVATE', provider: 'deepseek', model: 'deepseek-chat', requests: 20, tokens_in: 2000, tokens_out: 200 },
++ { requester_type: 'service', requester_id: 'codes', provider: 'acme', model: 'acme-1', requests: 2, tokens_in: 200, tokens_out: 20 },
++ ];
++ const out = aggregateWeek({ rows, weekStart: Date.parse('2026-11-09T00:00:00Z'), k: 20 });
++ assert.strictEqual(out.week_start, '2026-11-09');
++ assert.strictEqual(out.week_end, '2026-11-16');
++ assert.strictEqual(out.totals.requests, 45, 'only the published cells count');
++ assert.strictEqual(out.categories.length, 2);
++ assert.strictEqual(out.categories[0].category, 'live');
++ assert.strictEqual(out.categories.find((c) => c.category === 'apps').requests, 20, 'every app is one category');
++ assert.strictEqual(out.offers.length, 1, 'only the published provider/model cell remains');
++ assert.strictEqual(out.withheld.cells, 2, 'the small service cells are withheld');
++ assert.strictEqual(out.withheld.requests, 7);
++ const share = out.categories.reduce((n, c) => n + c.request_share, 0);
++ assert(Math.abs(share - 1) < 1e-9, 'published category shares add to one');
++ assert(!JSON.stringify(out).includes('prj_PRIVATE'), 'no project or app id is published');
++ assert.strictEqual(categoryOf({ requester_type: 'service', requester_id: 'live' }), 'live');
++});
++
++t.test('boot', async () => {
++ h = await boot();
++ const now = Date.now();
++ weekStart = lastCompleteWeek(now);
++ assert.strictEqual(mondayOf(weekStart), weekStart);
++});
++
++t.test('a completed week is stored once and the read is public and cached', async () => {
++ await addTraffic({ type: 'service', requester: 'live', provider: 'deepseek', model: 'deepseek-chat', count: 25 });
++ await addTraffic({ type: 'app', requester: 'prj_SECRET_APP', provider: 'deepseek', model: 'deepseek-chat', count: 20 });
++ await addTraffic({ type: 'service', requester: 'codes', provider: 'acme', model: 'acme-1', count: 10 });
++ // Opted out and failed traffic must not be in any number, not even the withheld total.
++ await addTraffic({ type: 'service', requester: 'live', provider: 'deepseek', model: 'deepseek-chat', count: 7, history: false });
++ await addTraffic({ type: 'service', requester: 'live', provider: 'deepseek', model: 'deepseek-chat', count: 4, status: 'failed' });
++
++ const first = await request(h.base, 'GET', '/api/v1/exchange/share');
++ assert.strictEqual(first.status, 200, first.text);
++ assert.strictEqual(first.headers.get('cache-control'), 'public, max-age=300');
++ const body = first.body;
++ assert.strictEqual(body.$schema, 'exchange.traffic-share@1');
++ assert.strictEqual(body.week_start, iso(weekStart).slice(0, 10));
++ assert(body.k >= 20);
++ assert.strictEqual(body.totals.requests, 45, 'opt-outs and failures are excluded');
++ assert.strictEqual(body.withheld.cells, 1);
++ assert.strictEqual(body.withheld.requests, 10);
++ assert.deepStrictEqual(body.categories.map((c) => c.category).sort(), ['apps', 'live']);
++ assert.strictEqual(body.offers.find((o) => o.provider === 'deepseek').requests, 45);
++ assert(!/prj_SECRET_APP/.test(first.text), 'no developer app is named');
++ assert(!/run_|rcpt_|usr_/.test(first.text), 'no run, receipt or person id leaves the read');
++ assert.strictEqual(body.offers.every((o) => o.request_share > 0 && o.request_share <= 1), true);
++
++ const second = await request(h.base, 'GET', '/api/v1/exchange/share');
++ assert(second.body.recomputed_at === null);
++ assert.strictEqual((await h.db.prepare('SELECT COUNT(*)::int AS n FROM exchange_traffic_share').get()).n, 1, 'the week is stored once, not recomputed on every read');
++});
++
++t.test('a named week is validated and old receipts are not a silent empty week', async () => {
++ const day = iso(weekStart).slice(0, 10);
++ const stored = await request(h.base, 'GET', `/api/v1/exchange/share?week=${day}`);
++ assert.strictEqual(stored.status, 200, stored.text);
++ assert.strictEqual(stored.body.week_start, day);
++ const tuesday = iso(weekStart + DAY_MS).slice(0, 10);
++ assert.strictEqual((await request(h.base, 'GET', `/api/v1/exchange/share?week=${tuesday}`)).status, 422);
++ const nextMonday = iso(mondayOf(Date.now())).slice(0, 10);
++ assert.strictEqual((await request(h.base, 'GET', `/api/v1/exchange/share?week=${nextMonday}`)).status, 422);
++ const old = iso(weekStart - 42 * DAY_MS).slice(0, 10);
++ assert.strictEqual((await request(h.base, 'GET', `/api/v1/exchange/share?week=${old}`)).status, 404);
++});
++
++(async () => {
++ try {
++ await t.run();
++ } finally {
++ if (h) await h.stop();
++ }
++})().catch((e) => { console.error(e); process.exit(1); });