openvibers/openvibe.exchange Commits
Publish compute prices beside model prices (#198)
Changes
@@@ -164,6 +164,7 @@ async function createApp(opts = {}) {app.use(cookieParser());// ── Machine endpoints ───────────────────────────────────++ app.get('/api/compute/prices', (_req, res) => res.set('Cache-Control', 'public, max-age=60').json(require('./compute-prices')));app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-exchange', name: 'OpenVibe.Exchange', version: VERSION }));// IndexNow's key file (/<key>.txt), when a key is configured.if (indexnow.enabled) app.use(indexnow.keyFile);nnew file mode 100644@@@ -0,0 +1,20 @@++'use strict';++// Reviewed public provider tariffs, 2026-11-11. Refresh before expiry; no automatic stale-price purchases.++const checked_at = '2026-11-11T00:00:00Z';++const expires_at = Date.parse('2026-11-18T00:00:00Z');++const cpu = (provider, region, hourly, source, extra = {}) => ({ id: `${provider}-${region}-2x4`, provider, region,++ kind: 'cpu', cpu: 2, ram_mb: 4096, gpu_mb: 0, currency: 'USD', hourly, per_second: hourly / 3600,++ billing_seconds: 1, checked_at, expires_at, source, ...extra });++const prices = [++ cpu('baseten', 'us-pdx-1', 2 * .0655 + 4 * .0211, 'https://www.baseten.co/pricing', { promotion: 'Free until November 16; owner must set expiry', promotion_expires: '2026-11-16T00:00:00Z' }),++ cpu('baseten', 'eu-lon-1', 2 * .0655 + 4 * .0211, 'https://www.baseten.co/pricing', { promotion_expires: '2026-11-16T00:00:00Z' }),++ cpu('daytona', 'us', 2 * .0504 + 4 * .0162, 'https://www.daytona.io/pricing', { disk_gb: 5, promotion: '$200 one-time credit; expiry must be confirmed in the account' }),++ cpu('daytona', 'eu', 2 * .0504 + 4 * .0162, 'https://www.daytona.io/pricing', { disk_gb: 5 }),++ cpu('e2b', 'provider-default', 3600 * (2 * .000014 + 4 * .0000045), 'https://e2b.dev/pricing', { promotion: '$100 one-time Hobby credit; 10 concurrent; 1-hour sessions' }),++ // Fly publishes 2 shared vCPU / 2 GiB at $13.39 for 720 hours in iad/ewr.++ ...['iad','ewr'].map(region => cpu('fly', region, 13.39 / 720, 'https://docs.fly.io/about/pricing', { id: `fly-${region}-2x2`, ram_mb: 2048 })),++ { id: 'daytona-4090-preemptible', provider: 'daytona', kind: 'gpu', gpu: 'RTX 4090', gpu_mb: 24576, currency: 'USD', hourly: .57, per_second: .57/3600, interruptible: true, checked_at, expires_at, source: 'https://www.daytona.io/pricing' },++ { id: 'daytona-4090-ondemand', provider: 'daytona', kind: 'gpu', gpu: 'RTX 4090', gpu_mb: 24576, currency: 'USD', hourly: .99, per_second: .99/3600, interruptible: false, checked_at, expires_at, source: 'https://www.daytona.io/pricing' },++ { id: 'runpod-4090-pod', provider: 'runpod', kind: 'gpu', gpu: 'RTX 4090', gpu_mb: 24576, currency: 'USD', hourly: .89, per_second: .89/3600, checked_at, expires_at, source: 'https://www.runpod.io/pricing' },++];++module.exports = { checked_at, prices };@@@ -72,6 +72,12 @@ function createPageRoutes(ctx) {return catalog;};++ r.get('/compute', (req, res) => {++ const { prices, checked_at } = require('../compute-prices');++ const rows = prices.map(p => `<tr><td>${esc(p.provider)}</td><td>${esc(p.id)}</td><td>${esc(p.gpu || `${p.cpu} vCPU / ${p.ram_mb} MiB`)}</td><td>$${p.hourly.toFixed(6)}/hour</td><td>$${p.per_second.toFixed(9)}/second</td><td><a href="${esc(p.source)}">Provider price</a></td></tr>`).join('');++ return page(req,res,{title:'Compute prices',body:raw(`<main><h1>Compute prices</h1><p>Checked ${esc(checked_at)}. Base compute only; storage, network and plan fees may apply. Promotional balances are account-specific.</p><p><a href="/">Model prices</a> · <a href="https://openvibe.network/admin#ai">Owner provider settings</a></p><div style="overflow-x:auto"><table><thead><tr><th>Provider</th><th>Price ID</th><th>Resources</th><th>Hourly</th><th>Per second</th><th>Source</th></tr></thead><tbody>${rows}</tbody></table></div></main>`)});++ });++// ── Exchange v1, retired ──────────────────────────────────function retiredHandler(req, res) {res.set('Cache-Control', 'public, max-age=86400');@@@ -78,7 +78,7 @@ function pricingPage() {}function home({ catalog, usage, viewer }) {const models = listed(catalog);-- return `<div class="ex-wrap xp">${unavailable(catalog)}<section class="xp-hero"><h1>One API for every model.<br>Pay what the model costs.</h1>${kit.find({ action: '/models', name: 'q', placeholder: 'Find a model', icon: 'search' })}<p>${kit.button({ href: viewer?.kind === 'user' ? '/dashboard/keys#new' : '/auth/login?next=%2Fdashboard%2Fkeys%23new', label: 'Get a key' })} ${kit.button({ href: '/docs', label: 'Read the docs', kind: 'quiet' })}</p>${code()}</section>${kit.stats(++ return `<div class="ex-wrap xp">${unavailable(catalog)}<section class="xp-hero"><h1>One API for every model.<br>Pay what the model costs.</h1>${kit.find({ action: '/models', name: 'q', placeholder: 'Find a model', icon: 'search' })}<p>${kit.button({ href: viewer?.kind === 'user' ? '/dashboard/keys#new' : '/auth/login?next=%2Fdashboard%2Fkeys%23new', label: 'Get a key' })} ${kit.button({ href: '/docs', label: 'Read the docs', kind: 'quiet' })} ${kit.button({ href: '/compute', label: 'Compute prices', kind: 'quiet' })}</p>${code()}</section>${kit.stats([[models.length, 'models'],[nnew file mode 100644@@@ -0,0 +1,16 @@++'use strict';++const assert=require('node:assert/strict');++const {boot}=require('./helpers/boot');++const {createFakeAi}=require('./helpers/ai');++(async()=>{++ const t=await boot({ai:createFakeAi()});++ try {++ const r=await t.get('/api/compute/prices');assert.equal(r.status,200,r.text);++ const data=JSON.parse(r.text);assert.equal(data.prices.length,10);++ assert.equal(data.prices.find(p=>p.id==='daytona-4090-preemptible').interruptible,true);++ for(const p of data.prices){assert.ok(p.hourly>0);assert.ok(p.expires_at>Date.parse(p.checked_at));assert.ok(p.source.startsWith('https://'));assert.equal(p.per_second,p.hourly/3600);}++ const page=await t.get('/compute');assert.equal(page.status,200);assert.match(page.text,/Compute prices/);assert.match(page.text,/Model prices/);++ assert.match((await t.get('/')).text,/href="\/compute"/);++ process.stdout.write('compute catalogue: dated source prices, spot distinction, units and model/compute links passed\n');++ }finally{await t.close();}++})().catch(e=>{process.stderr.write(String(e.stack)+'\n');process.exitCode=1;});
@@ -164,6 +164,7 @@ async function createApp(opts = {}) {
@@ -164,6 +164,7 @@ async function createApp(opts = {}) {
app.use(cookieParser());
app.use(cookieParser());
// ── Machine endpoints ───────────────────────────────────
// ── Machine endpoints ───────────────────────────────────
+ app.get('/api/compute/prices', (_req, res) => res.set('Cache-Control', 'public, max-age=60').json(require('./compute-prices')));
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-exchange', name: 'OpenVibe.Exchange', version: VERSION }));
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-exchange', name: 'OpenVibe.Exchange', version: VERSION }));
// IndexNow's key file (/<key>.txt), when a key is configured.
// IndexNow's key file (/<key>.txt), when a key is configured.
if (indexnow.enabled) app.use(indexnow.keyFile);
if (indexnow.enabled) app.use(indexnow.keyFile);
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+'use strict';
+// Reviewed public provider tariffs, 2026-11-11. Refresh before expiry; no automatic stale-price purchases.
+const checked_at = '2026-11-11T00:00:00Z';
+const expires_at = Date.parse('2026-11-18T00:00:00Z');
+const cpu = (provider, region, hourly, source, extra = {}) => ({ id: `${provider}-${region}-2x4`, provider, region,
+ kind: 'cpu', cpu: 2, ram_mb: 4096, gpu_mb: 0, currency: 'USD', hourly, per_second: hourly / 3600,
+ billing_seconds: 1, checked_at, expires_at, source, ...extra });
+const prices = [
+ cpu('baseten', 'us-pdx-1', 2 * .0655 + 4 * .0211, 'https://www.baseten.co/pricing', { promotion: 'Free until November 16; owner must set expiry', promotion_expires: '2026-11-16T00:00:00Z' }),
+ cpu('baseten', 'eu-lon-1', 2 * .0655 + 4 * .0211, 'https://www.baseten.co/pricing', { promotion_expires: '2026-11-16T00:00:00Z' }),
+ cpu('daytona', 'us', 2 * .0504 + 4 * .0162, 'https://www.daytona.io/pricing', { disk_gb: 5, promotion: '$200 one-time credit; expiry must be confirmed in the account' }),
+ cpu('daytona', 'eu', 2 * .0504 + 4 * .0162, 'https://www.daytona.io/pricing', { disk_gb: 5 }),
+ cpu('e2b', 'provider-default', 3600 * (2 * .000014 + 4 * .0000045), 'https://e2b.dev/pricing', { promotion: '$100 one-time Hobby credit; 10 concurrent; 1-hour sessions' }),
+ // Fly publishes 2 shared vCPU / 2 GiB at $13.39 for 720 hours in iad/ewr.
+ ...['iad','ewr'].map(region => cpu('fly', region, 13.39 / 720, 'https://docs.fly.io/about/pricing', { id: `fly-${region}-2x2`, ram_mb: 2048 })),
+ { id: 'daytona-4090-preemptible', provider: 'daytona', kind: 'gpu', gpu: 'RTX 4090', gpu_mb: 24576, currency: 'USD', hourly: .57, per_second: .57/3600, interruptible: true, checked_at, expires_at, source: 'https://www.daytona.io/pricing' },
+ { id: 'daytona-4090-ondemand', provider: 'daytona', kind: 'gpu', gpu: 'RTX 4090', gpu_mb: 24576, currency: 'USD', hourly: .99, per_second: .99/3600, interruptible: false, checked_at, expires_at, source: 'https://www.daytona.io/pricing' },
+ { id: 'runpod-4090-pod', provider: 'runpod', kind: 'gpu', gpu: 'RTX 4090', gpu_mb: 24576, currency: 'USD', hourly: .89, per_second: .89/3600, checked_at, expires_at, source: 'https://www.runpod.io/pricing' },
+];
+module.exports = { checked_at, prices };
@@ -72,6 +72,12 @@ function createPageRoutes(ctx) {
@@ -72,6 +72,12 @@ function createPageRoutes(ctx) {
return catalog;
return catalog;
};
};
+ r.get('/compute', (req, res) => {
+ const { prices, checked_at } = require('../compute-prices');
+ const rows = prices.map(p => `<tr><td>${esc(p.provider)}</td><td>${esc(p.id)}</td><td>${esc(p.gpu || `${p.cpu} vCPU / ${p.ram_mb} MiB`)}</td><td>$${p.hourly.toFixed(6)}/hour</td><td>$${p.per_second.toFixed(9)}/second</td><td><a href="${esc(p.source)}">Provider price</a></td></tr>`).join('');
+ return page(req,res,{title:'Compute prices',body:raw(`<main><h1>Compute prices</h1><p>Checked ${esc(checked_at)}. Base compute only; storage, network and plan fees may apply. Promotional balances are account-specific.</p><p><a href="/">Model prices</a> · <a href="https://openvibe.network/admin#ai">Owner provider settings</a></p><div style="overflow-x:auto"><table><thead><tr><th>Provider</th><th>Price ID</th><th>Resources</th><th>Hourly</th><th>Per second</th><th>Source</th></tr></thead><tbody>${rows}</tbody></table></div></main>`)});
+ });
+
// ── Exchange v1, retired ──────────────────────────────────
// ── Exchange v1, retired ──────────────────────────────────
function retiredHandler(req, res) {
function retiredHandler(req, res) {
res.set('Cache-Control', 'public, max-age=86400');
res.set('Cache-Control', 'public, max-age=86400');
@@ -78,7 +78,7 @@ function pricingPage() {
@@ -78,7 +78,7 @@ function pricingPage() {
}
}
function home({ catalog, usage, viewer }) {
function home({ catalog, usage, viewer }) {
const models = listed(catalog);
const models = listed(catalog);
- return `<div class="ex-wrap xp">${unavailable(catalog)}<section class="xp-hero"><h1>One API for every model.<br>Pay what the model costs.</h1>${kit.find({ action: '/models', name: 'q', placeholder: 'Find a model', icon: 'search' })}<p>${kit.button({ href: viewer?.kind === 'user' ? '/dashboard/keys#new' : '/auth/login?next=%2Fdashboard%2Fkeys%23new', label: 'Get a key' })} ${kit.button({ href: '/docs', label: 'Read the docs', kind: 'quiet' })}</p>${code()}</section>${kit.stats(
+ return `<div class="ex-wrap xp">${unavailable(catalog)}<section class="xp-hero"><h1>One API for every model.<br>Pay what the model costs.</h1>${kit.find({ action: '/models', name: 'q', placeholder: 'Find a model', icon: 'search' })}<p>${kit.button({ href: viewer?.kind === 'user' ? '/dashboard/keys#new' : '/auth/login?next=%2Fdashboard%2Fkeys%23new', label: 'Get a key' })} ${kit.button({ href: '/docs', label: 'Read the docs', kind: 'quiet' })} ${kit.button({ href: '/compute', label: 'Compute prices', kind: 'quiet' })}</p>${code()}</section>${kit.stats(
[
[
[models.length, 'models'],
[models.length, 'models'],
[
[
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+'use strict';
+const assert=require('node:assert/strict');
+const {boot}=require('./helpers/boot');
+const {createFakeAi}=require('./helpers/ai');
+(async()=>{
+ const t=await boot({ai:createFakeAi()});
+ try {
+ const r=await t.get('/api/compute/prices');assert.equal(r.status,200,r.text);
+ const data=JSON.parse(r.text);assert.equal(data.prices.length,10);
+ assert.equal(data.prices.find(p=>p.id==='daytona-4090-preemptible').interruptible,true);
+ for(const p of data.prices){assert.ok(p.hourly>0);assert.ok(p.expires_at>Date.parse(p.checked_at));assert.ok(p.source.startsWith('https://'));assert.equal(p.per_second,p.hourly/3600);}
+ const page=await t.get('/compute');assert.equal(page.status,200);assert.match(page.text,/Compute prices/);assert.match(page.text,/Model prices/);
+ assert.match((await t.get('/')).text,/href="\/compute"/);
+ process.stdout.write('compute catalogue: dated source prices, spot distinction, units and model/compute links passed\n');
+ }finally{await t.close();}
+})().catch(e=>{process.stderr.write(String(e.stack)+'\n');process.exitCode=1;});