openvibers/openvibe.ai Commits
Read public GPU marketplace prices and the token cost they imply (#406)
Changes
@@@ -88,6 +88,45 @@ state, the measured cold start (from the launch to the first answered request),with its price (the hour and the second), endpoint, cold start and money. "Start" and "Stop" write a request row;the reconciler (one implementation, the same one the service runs) does the work.++## Public marketplace prices, and what a token costs++++The console's GPUs and supply sections also show what the cards cost on the public marketplaces, read from the++providers' own pages by `server/gpu/market.js` (migration `0061_gpu_market_prices.sql`):++++| Source | Public page | What it publishes |++| --- | --- | --- |++| Vast.ai | `POST https://console.vast.ai/api/v0/bundles/` | the offer search (on demand and the minimum interruptible bid), with region and reliability |++| RunPod | `https://www.runpod.io/pricing` | the Secure and Community Cloud on-demand columns (neither is a spot quote) |++| Salad | `https://salad.com/pricing/` | High (never preempted) and Lowest (most interruptible) priority tiers |++| TensorDock | `https://www.tensordock.com/` | the public "from" prices for its most popular enterprise cards |++| CloudRift | `https://www.cloudrift.ai/pricing` | on-demand rates; H100 and H200 are quoted on request |++++The cards are RTX 3090, RTX 4090, RTX 5090, A100 80GB, H100 and H200. For each one the report gives the lowest++on-demand and the lowest interruptible/spot hour, the provider, the region when the provider states it, the++reliability words or score it publishes, and when it was read. The job signs in nowhere, rents nothing and stops++nothing. It refreshes when due (six hours by default) and is off in tests and under a restore drill. A source that++fails keeps its last rows and is marked `stale`; the console can still show the last known price, labelled.++++Both readings and the per-token cost they imply are on the admin API:++++- `GET /api/v1/admin/console/gpus`: `state` (the provisioner) plus `market` and `token_costs`;++- `GET /api/v1/admin/console/supply`: the supply report plus the same `market` and `token_costs`;++- `POST /api/v1/admin/console/gpus/prices/refresh`: read the public pages now (operator capability; still no rent).++++`token_costs` is one row per catalog model, GPU it fits, price kind and provider reading:++++```++USD per million output tokens = hourly USD × 1,000,000 / throughput tokens per hour++```++++The throughput and its source are on the row (the catalog's planning estimate today;++`server/gpu/catalog.js`). `is_cheapest` marks the fresh floor for that model, card and kind. Each row also carries++`cheapest_api`: the cheapest published API price for the same model, the same model family and weights++(`same_family_weights`), the same family, or the same uncensored weights. If no comparable API model has a price,++it is `null` rather than a guess. The console only renders this data; the page design stays in Exchange.++++Test coverage is `test/gpu-market.test.js` with saved pages in `test/fixtures/gpu-market/`; it makes no live call.++## Your own endpoint (the private rent)The Exchange's product line 6, "your own endpoint", is the same provisioner with a private rent on top@@@ -38,6 +38,12 @@ records: `provider` (vast/runpod), `gpu`, `region`, `hourly_usd`, `vram_mb`, `soIt requires the same operator capability as provider configuration. The parser reads bytes only. Stale quotes (over sevendays) and missing throughput never cause a rent proposal. Recording listings does not rent anything or contact providers.++The same two console reads also carry the public marketplace readings and the per-token cost they imply:++`market` (Vast.ai, RunPod, Salad, TensorDock and CloudRift, cached in `gpu_market_prices`) and `token_costs`++(catalog throughput × hourly price, beside `cheapest_api`). The reader is `server/gpu/market.js`; it reads public++pages only, never signs in, never rents, and a failed source keeps its last rows marked `stale`. `POST++/api/v1/admin/console/gpus/prices/refresh` forces a read. See [ai-gpus.md](ai-gpus.md#public-marketplace-prices-and-what-a-token-costs).++Migration 0052 is expand-only. These changes need money/security review before deployment; this PR does not deploy.## Public market observationsnnew file mode 100644@@@ -0,0 +1,28 @@++-- phase: expand++-- The public GPU marketplace prices the owner compares against API tokens. One row is the lowest price a++-- provider publishes right now for one card and one kind; a source that fails keeps its rows, and++-- gpu_market_sources records that the last read failed so the console can mark them stale.++CREATE TABLE IF NOT EXISTS gpu_market_prices (++ provider text NOT NULL,++ gpu_key text NOT NULL,++ gpu text NOT NULL,++ kind text NOT NULL CHECK(kind IN ('on_demand', 'spot')),++ hourly_usd double precision NOT NULL CHECK(hourly_usd > 0),++ region text NOT NULL DEFAULT '',++ reliability double precision,++ note text NOT NULL DEFAULT '',++ variant text NOT NULL DEFAULT '',++ source text NOT NULL,++ observed_at text NOT NULL,++ PRIMARY KEY(provider, gpu_key, kind)++);++CREATE TABLE IF NOT EXISTS gpu_market_sources (++ provider text PRIMARY KEY,++ label text NOT NULL,++ source text NOT NULL,++ status text NOT NULL CHECK(status IN ('ok', 'failed')),++ read_at text,++ ok_at text,++ error text,++ offers integer NOT NULL DEFAULT 0++);@@@ -7,11 +7,20 @@ const { AiError } = require('../util');const providerPresets = require('../console/presets');const providerCatalog = require('../providers/catalog');const poolSeed = require('../providers/pool-seed');++const gpuCatalog = require('../gpu/catalog');++const { tokenCosts } = require('../gpu/token-cost');/** Read-only JSON read models for Exchange's owner console. Every query is bounded and the query module redacts runs. */--function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, gpus = null, abuse = null, priceBook = null, creditGrants = null, sessionCookies = null, vault = null, settings = null, config = {}, log = console }) {++function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, gpus = null, gpuMarket = null, abuse = null, priceBook = null, creditGrants = null, sessionCookies = null, vault = null, settings = null, config = {}, log = console }) {const r = express.Router();const supply = require('../platform/supply').createSupply({ db, registry, clock });++ // The public marketplace readings and the per-token cost they imply, added to both console sections. Data only:++ // Exchange renders it with its existing console classes.++ const marketPayload = async () => {++ if (!gpuMarket) return { market: null, token_costs: [] };++ const market = await gpuMarket.report();++ return { market, token_costs: tokenCosts({ market, models: await registry.listModels(), catalog: gpuCatalog }) };++ };const json = express.json({ limit: '64kb' });const read = auth.requireCap(CAPS.usageRead);const write = auth.requireCap(CAPS.providerManage);@@@ -26,7 +35,7 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,};r.get('/api/v1/admin/console/:section', read, wrap(async (req) => {const section = req.params.section;-- if (section === 'supply') return supply.report();++ if (section === 'supply') return { ...await supply.report(), ...await marketPayload() };if (section === 'overview') {const since = new Date(clock.now() - 86400000).toISOString();return { status: await ops.status(), counts24: await q.statusCounts(db, since), failures24: await q.failures(db, since), queue: runs.stats(), cache: await cache.stats() };@@@ -70,7 +79,7 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,const result = await q.listAudit(db, filters);return { rows: result.rows, next: result.next, filters: form, invalid_filters: bad, kinds: q.AUDIT_KINDS };}-- if (section === 'gpus') return { state: gpus ? await gpus.state() : null };++ if (section === 'gpus') return { state: gpus ? await gpus.state() : null, ...await marketPayload() };if (section === 'pricebook') return { report: priceBook ? priceBook.last : null, state: priceBook ? await priceBook.state.read() : null,pending: priceBook ? await priceBook.pending(50) : [], recent: priceBook ? await priceBook.changes({ status: 'applied', limit: 50 }) : [] };if (section === 'exchange') return { credits: await q.settleQueue(db, clock.now()), routing: await q.routingEvidence(db) };@@@ -82,6 +91,11 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,if (!gpus) throw new AiError(409, 'ai.conflict', 'GPU quotes are off on this host');return await gpus.preview(String(req.params.model), { strategy: req.query.strategy || undefined });}));++ // Read the public marketplace pages now instead of waiting for the due tick. It still never rents anything.++ r.post('/api/v1/admin/console/gpus/prices/refresh', write, wrap(async () => {++ if (!gpuMarket) throw new AiError(409, 'ai.conflict', 'GPU price sources are off on this host');++ return { ...await gpuMarket.refresh(), ...await marketPayload() };++ }));r.post('/api/v1/admin/console/supply/quotes', json, write, wrap(async req => ({ recorded: await require('../pricebook/gpus').recordGpuQuotes(db, JSON.stringify(req.body.quotes)), dry_run: true })));r.post('/api/v1/admin/console/supply', json, write, wrap(req => supply.save(req.body || {}, actor(req))));r.post('/api/v1/admin/console/providers/:key/key', json, write, wrap(async (req) => {@@@ -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, 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 }) {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') });@@@ -232,7 +232,7 @@ function createApp({ choices = null, searchFetch = null, config, db, registry, p// dashboard, an API for Exchange. Nothing here writes an offer; the offer belongs to Network and the Link owner.if (community) app.use('/api/v1', require('./api/community').communityRouter({ community, auth, log }));app.use(adminRouter({ config, db, registry, pool, quotas, cache, runs, auth, settings, ops, log }));-- app.use(require('./api/console-data').consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth, clock, fetchImpl, gpus, abuse, priceBook, creditGrants, sessionCookies, vault, settings, config, log }));++ app.use(require('./api/console-data').consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth, clock, fetchImpl, gpus, gpuMarket, abuse, priceBook, creditGrants, sessionCookies, vault, settings, config, log }));// The operator console moved to OpenVibe.Exchange. Keep the legacy AI paths as permanent redirects;// run receipts still use the person-session routes below and the AI JSON admin APIs stay here.// Person sessions are separate, read-only and open one run receipt each (server/person-session.js).@@@ -418,6 +418,21 @@ function load(env = process.env) {thresholdPct: Number(env.AI_PRICE_BOOK_THRESHOLD_PCT || 20) / 100,},++ // ── Public GPU marketplace prices (server/gpu/market.js) ──++ // The small job that reads Vast.ai, RunPod, Salad, TensorDock and CloudRift's public pages for the cards the++ // provisioner uses (and the next sizes up). It never signs in, rents or stops anything. The readings are++ // cached in gpu_market_prices; a failed source keeps its last rows, marked stale. Off under a drill and in++ // test mode, where tests replay saved pages.++ gpuMarket: {++ enabled: bool(env.AI_GPU_MARKET, !bool(env.AI_DRILL, false) && nodeEnv !== 'test'),++ // How long a reading stays fresh, and how often the job looks whether a refresh is due.++ intervalMs: Math.max(3600000, int(env.AI_GPU_MARKET_INTERVAL_MS, 6 * 60 * 60 * 1000)),++ tickMs: Math.max(60000, int(env.AI_GPU_MARKET_TICK_MS, 60 * 60 * 1000)),++ firstTickMs: Math.max(1000, int(env.AI_GPU_MARKET_FIRST_TICK_MS, 2 * 60 * 1000)),++ staleMs: Math.max(3600000, int(env.AI_GPU_MARKET_STALE_MS, 12 * 60 * 60 * 1000)),++ timeoutMs: Math.max(1000, int(env.AI_GPU_MARKET_TIMEOUT_MS, 20000)),++ },++// ── Hourly provider balance probe (server/providers/balance.js) ──// Each active provider with a balance API (DeepSeek today) is asked GET <origin>/user/balance and the reading// is exposed as ai_provider_balance_usd{provider} on GET /metrics. Off under a restore drill (AI_DRILL=1).@@@ -23,6 +23,8 @@ const MODELS = Object.freeze([provider_key: 'gpu.deepseek-r1-32b-abliterated',model_key: 'deepseek-r1-32b-abliterated',marketplace: 'vast',++ family: 'deepseek',++ uncensored: true,context_window: 8192,throughput_tokens_per_hour: 180000,requirements: { vram_mb: 22000, disk_gb: 60, gpu_names: ['RTX 3090', 'RTX 4090'], num_gpus: 1, min_reliability: 0.99 },@@@ -46,6 +48,8 @@ const MODELS = Object.freeze([provider_key: 'gpu.deepseek-r1-32b',model_key: 'deepseek-r1-32b',marketplace: 'vast',++ family: 'deepseek',++ uncensored: false,context_window: 8192,throughput_tokens_per_hour: 180000,requirements: { vram_mb: 22000, disk_gb: 60, gpu_names: ['RTX 3090', 'RTX 4090'], num_gpus: 1, min_reliability: 0.99 },nnew file mode 100644@@@ -0,0 +1,440 @@++'use strict';++/**++ * Public GPU marketplace prices: what the cards the provisioner uses (and the next sizes up) cost per hour,++ * on demand and interruptible, read from the providers' own public pages.++ *++ * Vast.ai POST /api/v0/bundles/ the public offer search (no account is needed to search)++ * RunPod GET /pricing the public price table (secure and community columns)++ * Salad GET /pricing/ the public priority tiers (high is never preempted, lowest is)++ * TensorDock GET / the public "from" prices on its home page++ * CloudRift GET /pricing the public on-demand table (H100/H200 are quoted on request)++ *++ * The rules:++ *++ * - Read only public pages. No account, key, rental, start or stop is ever made here.++ * - One row per provider, card and kind: the lowest price that provider publishes right now. A source that++ * cannot be read changes nothing; its last rows stay and are marked stale until it answers again.++ * - The reading carries where it came from, when it was read, the region when the provider states one, and++ * the reliability words the provider publishes. A number without those is not a price the owner can trust.++ *++ * The cache is in the database (migrations/0061_gpu_market_prices.sql) because AI is deployed several times a++ * day: a timer in the process would be reset by every deploy. The refresh is due-based, like the price book.++ */++const { AiError } = require('../util');++++const HOUR_MS = 60 * 60 * 1000;++const DEFAULT_INTERVAL_MS = 6 * HOUR_MS;++const DEFAULT_TICK_MS = HOUR_MS;++const DEFAULT_FIRST_TICK_MS = 2 * 60 * 1000;++const DEFAULT_STALE_MS = 12 * HOUR_MS;++const DEFAULT_TIMEOUT_MS = 20000;++const MAX_BYTES = 4 * 1024 * 1024;++++/**++ * The cards the owner asked about: the two the catalog runs on, and the next sizes up. `minVramMb` keeps a++ * 40 GB A100 out of the 80 GB row (Vast lists both under the same family names).++ */++const TARGETS = Object.freeze([++ { key: 'rtx-3090', gpu: 'RTX 3090', vram_mb: 24576, minVramMb: 24000, aliases: ['RTX 3090'] },++ { key: 'rtx-4090', gpu: 'RTX 4090', vram_mb: 24576, minVramMb: 24000, aliases: ['RTX 4090'] },++ { key: 'rtx-5090', gpu: 'RTX 5090', vram_mb: 32768, minVramMb: 32000, aliases: ['RTX 5090'] },++ { key: 'a100-80gb', gpu: 'A100 80GB', vram_mb: 81920, minVramMb: 80000, aliases: ['A100 SXM4', 'A100 PCIE', 'A100 80GB', 'A100-SXM4', 'A100-PCIE'] },++ { key: 'h100', gpu: 'H100', vram_mb: 81920, minVramMb: 80000, aliases: ['H100 SXM', 'H100 SXM5', 'H100 PCIE', 'H100 NVL', 'H100 80GB'] },++ { key: 'h200', gpu: 'H200', vram_mb: 141312, minVramMb: 140000, aliases: ['H200', 'H200 NVL', 'H200 SXM'] },++]);++const BY_KEY = new Map(TARGETS.map((t) => [t.key, t]));++++const VAST_NAMES = TARGETS.flatMap((t) => t.aliases.filter((a) => !/^(A100 80GB|H100 80GB|H100 SXM5)$/.test(a)));++++function nameOf(value) {++ return String(value == null ? '' : value).trim().toUpperCase().replace(/\s+/g, ' ');++}++++/** The target a marketplace name (and, when it publishes one, its memory) names, or null. */++function targetFor(name, memoryMb = null) {++ const normalized = nameOf(name);++ for (const target of TARGETS) {++ if (!target.aliases.some((a) => nameOf(a) === normalized)) continue;++ if (memoryMb != null && Number.isFinite(Number(memoryMb)) && Number(memoryMb) < target.minVramMb) continue;++ return target;++ }++ return null;++}++++function usd(value) {++ if (value === null || value === undefined || value === '') return null;++ const n = Number(value);++ return Number.isFinite(n) && n > 0 ? n : null;++}++++function reliabilityOf(value) {++ if (value == null || value === '') return null;++ const n = Number(value);++ return Number.isFinite(n) && n >= 0 && n <= 1 ? n : null;++}++++/** A reading in the one shape every source returns. */++function reading(target, kind, price, { region = '', reliability = null, note = '', source, variant = '' } = {}) {++ if (!target || !usd(price)) return null;++ return {++ gpu_key: target.key,++ gpu: target.gpu,++ vram_mb: target.vram_mb,++ kind,++ hourly_usd: usd(price),++ region: String(region || ''),++ reliability,++ note,++ variant: String(variant || ''),++ source,++ };++}++++/** Vast.ai's public offer JSON: one reading per target, the caller keeps the lowest per provider/key/kind. */++function parseVast(bytes, kind) {++ const body = typeof bytes === 'string' ? JSON.parse(bytes) : bytes;++ const offers = (body && body.offers) || [];++ const out = [];++ for (const offer of offers) {++ // dph_total is the whole single-GPU hour (GPU plus the offer's storage); min_bid is the interruptible bid,++ // with storage billed on top. A multi-GPU offer's total is not comparable with a one-GPU hour.++ if (Number(offer.num_gpus) !== 1) continue;++ const target = targetFor(offer.gpu_name, offer.gpu_ram);++ if (!target) continue;++ const price = kind === 'spot' ? offer.min_bid : offer.dph_total;++ const verification = String(offer.verification || 'unknown');++ const note = kind === 'spot'++ ? `minimum bid; storage billed on top; ${verification} host`++ : `total includes the offer's storage; ${verification} host`;++ const row = reading(target, kind, price, {++ region: offer.geolocation || '',++ reliability: reliabilityOf(offer.reliability2 == null ? offer.reliability : offer.reliability2),++ note,++ variant: String(offer.gpu_name || ''),++ source: 'https://vast.ai/pricing',++ });++ if (row) out.push(row);++ }++ return out;++}++++/** RunPod publishes on-demand rates for both Secure and Community Cloud; neither column is a spot quote. */++function parseRunpod(html) {++ const out = [];++ for (const chunk of String(html).split(/class="gpu_table-row"/).slice(1)) {++ const name = (chunk.match(/class="gpu_name[^"]*">([^<]+)</) || [])[1];++ if (!name) continue;++ const target = targetFor(name);++ if (!target) continue;++ const secure = (chunk.match(/data-secure="([\d.]+)"/) || [])[1];++ const community = (chunk.match(/data-community="([\d.]+)"/) || [])[1];++ const on = reading(target, 'on_demand', secure, {++ note: 'secure cloud (verified hosts)', variant: name.trim(), source: 'https://www.runpod.io/pricing',++ });++ const communityRate = reading(target, 'on_demand', community, {++ note: 'community cloud on-demand (third-party hosts; no spot quote published)',++ variant: name.trim(), source: 'https://www.runpod.io/pricing',++ });++ if (on) out.push(on);++ if (communityRate) out.push(communityRate);++ }++ return out;++}++++/** Salad's price table: High is never preempted; Lowest is the most interruptible. */++function parseSalad(html) {++ const match = String(html).match(/<script[^>]*id=["']?gpu-prices["']?[^>]*>([\s\S]*?)<\/script>/i);++ if (!match) throw new Error('Salad pricing page has no gpu-prices table');++ const rows = JSON.parse(match[1]);++ const out = [];++ for (const row of Array.isArray(rows) ? rows : []) {++ const target = targetFor(row && row.name);++ if (!target) continue;++ const on = reading(target, 'on_demand', row.high, {++ note: 'high priority tier (never preempted by other workloads); vCPU and RAM included',++ variant: String(row.name || ''), source: 'https://salad.com/pricing/',++ });++ const spot = reading(target, 'spot', row.lowest, {++ note: 'lowest priority tier (the most interruptible); vCPU and RAM included',++ variant: String(row.name || ''), source: 'https://salad.com/pricing/',++ });++ if (on) out.push(on);++ if (spot) out.push(spot);++ }++ return out;++}++++/** TensorDock's home page publishes a "from" price for its two most popular enterprise cards. */++function parseTensordock(html) {++ const text = String(html).replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')++ .replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/\s+/g, ' ');++ const out = [];++ for (const [key, re] of [++ ['h100', /H100 SXM5\s+From\s+\$([\d.]+)\s*\/hr/i],++ ['a100-80gb', /A100 SXM4\s+From\s+\$([\d.]+)\s*\/hr/i],++ ]) {++ const m = text.match(re);++ if (!m) continue;++ const target = BY_KEY.get(key);++ const row = reading(target, 'on_demand', m[1], {++ note: 'published "from" price on the TensorDock home page; the full marketplace list is behind its dashboard; no reliability score published',++ variant: key === 'h100' ? 'H100 SXM5' : 'A100 SXM4',++ source: 'https://www.tensordock.com/',++ });++ if (row) out.push(row);++ }++ return out;++}++++/** CloudRift's public table: only the cards whose on-demand rate is a number; its H100/H200 say "Upon request". */++function parseCloudrift(html) {++ const text = String(html).replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')++ .replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/\s+/g, ' ');++ const out = [];++ for (const [name, key] of [['RTX 4090', 'rtx-4090'], ['RTX 5090', 'rtx-5090'], ['A100 SXM4', 'a100-80gb'], ['H100', 'h100'], ['H200', 'h200']]) {++ const normalized = name.replace(/ /g, '\\s+');++ const m = text.match(new RegExp(normalized + '\\s+NVIDIA\\s+On.?Demand\\s+\\$([\\d.]+)', 'i'));++ if (!m) continue;++ const row = reading(BY_KEY.get(key), 'on_demand', m[1], {++ note: 'on-demand rate from the public table; reserved rates are lower, H100/H200 on demand are quoted on request, and no reliability score is published',++ variant: name, source: 'https://www.cloudrift.ai/pricing',++ });++ if (row) out.push(row);++ }++ return out;++}++++async function read(fetchImpl, url, { method = 'GET', body = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {++ const headers = { Accept: 'application/json, text/html;q=0.9', 'User-Agent': 'OpenVibe.AI GPU price reader (+https://openvibe.exchange)' };++ if (body != null) headers['Content-Type'] = 'application/json';++ let res;++ try {++ res = await fetchImpl(url, {++ method, headers, redirect: 'follow', signal: AbortSignal.timeout(Math.max(1000, timeoutMs)),++ body: body != null ? JSON.stringify(body) : undefined,++ });++ } catch (err) {++ throw new Error(`${url} did not answer: ${String(err && err.message || err).slice(0, 120)}`);++ }++ if (!res || !res.ok) throw new Error(`${url} answered ${res ? res.status : 'no status'}`);++ const text = await res.text();++ if (text.length > MAX_BYTES) throw new Error(`${url} answered ${text.length} bytes (over ${MAX_BYTES})`);++ return text;++}++++const SOURCES = Object.freeze([++ {++ id: 'vast', label: 'Vast.ai', source: 'https://vast.ai/pricing',++ async read({ fetchImpl, timeoutMs }) {++ const body = (type, order) => ({++ type, gpu_name: { in: VAST_NAMES }, num_gpus: { eq: 1 }, rentable: { eq: true }, limit: 500,++ order: [[order, 'asc']],++ });++ const [onDemand, bid] = await Promise.all([++ read(fetchImpl, 'https://console.vast.ai/api/v0/bundles/', { method: 'POST', body: body('ondemand', 'dph_total'), timeoutMs }),++ read(fetchImpl, 'https://console.vast.ai/api/v0/bundles/', { method: 'POST', body: body('bid', 'min_bid'), timeoutMs }),++ ]);++ return [...parseVast(onDemand, 'on_demand'), ...parseVast(bid, 'spot')];++ },++ },++ {++ id: 'runpod', label: 'RunPod', source: 'https://www.runpod.io/pricing',++ async read({ fetchImpl, timeoutMs }) {++ return parseRunpod(await read(fetchImpl, 'https://www.runpod.io/pricing', { timeoutMs }));++ },++ },++ {++ id: 'salad', label: 'Salad', source: 'https://salad.com/pricing/',++ async read({ fetchImpl, timeoutMs }) {++ return parseSalad(await read(fetchImpl, 'https://salad.com/pricing/', { timeoutMs }));++ },++ },++ {++ id: 'tensordock', label: 'TensorDock', source: 'https://www.tensordock.com/',++ async read({ fetchImpl, timeoutMs }) {++ return parseTensordock(await read(fetchImpl, 'https://www.tensordock.com/', { timeoutMs }));++ },++ },++ {++ id: 'cloudrift', label: 'CloudRift', source: 'https://www.cloudrift.ai/pricing',++ async read({ fetchImpl, timeoutMs }) {++ return parseCloudrift(await read(fetchImpl, 'https://www.cloudrift.ai/pricing', { timeoutMs }));++ },++ },++]);++const SOURCE_BY_ID = new Map(SOURCES.map((s) => [s.id, s]));++++/** The lowest reading per (provider, card, kind): one row is what a provider actually charges for that hour. */++function lowest(readings) {++ const out = new Map();++ for (const row of readings || []) {++ if (!row || !usd(row.hourly_usd)) continue;++ const key = `${row.gpu_key}\u0000${row.kind}`;++ const held = out.get(key);++ if (!held || row.hourly_usd < held.hourly_usd) out.set(key, row);++ }++ return [...out.values()];++}++++function createGpuMarket({ db, config = {}, clock = { now: () => Date.now() }, fetchImpl = null, log = console } = {}) {++ const settings = {++ enabled: false,++ intervalMs: DEFAULT_INTERVAL_MS,++ tickMs: DEFAULT_TICK_MS,++ firstTickMs: DEFAULT_FIRST_TICK_MS,++ staleMs: DEFAULT_STALE_MS,++ timeoutMs: DEFAULT_TIMEOUT_MS,++ ...(config.gpuMarket || {}),++ };++ const iso = (at = clock.now()) => new Date(at).toISOString();++ let timer = null;++ let first = null;++ let busy = false;++++ /** Fetch every source and write what it said; a failed source leaves its last readings in place. */++ async function refresh({ fetchImpl: seam = null } = {}) {++ const fetcher = seam || fetchImpl;++ if (!fetcher) throw new AiError(503, 'gpu.market_unavailable', 'No GPU price transport is available');++ const outcomes = await Promise.all(SOURCES.map(async (source) => {++ try {++ const rows = await source.read({ fetchImpl: fetcher, timeoutMs: settings.timeoutMs });++ return { source, rows: lowest(rows) };++ } catch (err) {++ return { source, error: String(err && err.message || err).slice(0, 300) };++ }++ }));++ const at = iso();++ let refreshed = 0;++ let failed = 0;++ for (const outcome of outcomes) {++ const { source } = outcome;++ if (outcome.error) {++ failed += 1;++ await db.prepare(`INSERT INTO gpu_market_sources(provider,label,source,status,read_at,ok_at,error,offers)++ VALUES(?,?,?,?,?,NULL,?,0)++ ON CONFLICT(provider) DO UPDATE SET status=excluded.status, read_at=excluded.read_at,++ error=excluded.error, label=excluded.label, source=excluded.source`)++ .run(source.id, source.label, source.source, 'failed', at, outcome.error);++ continue;++ }++ refreshed += 1;++ await db.prepare('DELETE FROM gpu_market_prices WHERE provider = ?').run(source.id);++ for (const row of outcome.rows) {++ await db.prepare(`INSERT INTO gpu_market_prices(provider,gpu_key,gpu,kind,hourly_usd,region,reliability,note,variant,source,observed_at)++ VALUES(?,?,?,?,?,?,?,?,?,?,?)++ ON CONFLICT(provider,gpu_key,kind) DO UPDATE SET hourly_usd=excluded.hourly_usd, region=excluded.region,++ reliability=excluded.reliability, note=excluded.note, variant=excluded.variant, source=excluded.source,++ observed_at=excluded.observed_at`)++ .run(source.id, row.gpu_key, row.gpu, row.kind, row.hourly_usd, row.region, row.reliability,++ row.note, row.variant, row.source, at);++ }++ await db.prepare(`INSERT INTO gpu_market_sources(provider,label,source,status,read_at,ok_at,error,offers)++ VALUES(?,?,?,?,?,?,NULL,?)++ ON CONFLICT(provider) DO UPDATE SET status='ok', read_at=excluded.read_at, ok_at=excluded.ok_at,++ error=NULL, offers=excluded.offers, label=excluded.label, source=excluded.source`)++ .run(source.id, source.label, source.source, 'ok', at, at, outcome.rows.length);++ }++ return { refreshed, failed, sources: (await report()).sources };++ }++++ /** The cached readings, with the stale facts: a failed source keeps its last numbers, marked stale. */++ async function report() {++ const [sourceRows, priceRows] = await Promise.all([++ db.prepare('SELECT * FROM gpu_market_sources').all().catch(() => []),++ db.prepare('SELECT * FROM gpu_market_prices').all().catch(() => []),++ ]);++ const now = clock.now();++ const state = new Map((sourceRows || []).map((row) => [row.provider, row]));++ const sources = SOURCES.map((source) => {++ const row = state.get(source.id) || null;++ const okAt = row && row.ok_at ? Date.parse(row.ok_at) : NaN;++ const stale = !row || row.status !== 'ok' || !Number.isFinite(okAt) || now - okAt > settings.staleMs;++ return {++ provider: source.id,++ label: source.label,++ source: source.source,++ status: row ? String(row.status) : 'never',++ read_at: row && row.read_at ? String(row.read_at) : null,++ ok_at: row && row.ok_at ? String(row.ok_at) : null,++ stale,++ error: row && row.error ? String(row.error) : null,++ offers: row ? Number(row.offers) || 0 : 0,++ };++ });++ const sourceState = new Map(sources.map((s) => [s.provider, s]));++ const rows = (priceRows || []).map((row) => ({ ...row, hourly_usd: Number(row.hourly_usd), reliability: row.reliability == null ? null : Number(row.reliability) }))++ .filter((row) => usd(row.hourly_usd));++ const byTarget = new Map(TARGETS.map((t) => [t.key, []]));++ for (const row of rows) {++ if (!byTarget.has(row.gpu_key)) continue;++ const source = sourceState.get(row.provider);++ byTarget.get(row.gpu_key).push({++ provider: row.provider,++ provider_label: source ? source.label : row.provider,++ kind: row.kind,++ hourly_usd: row.hourly_usd,++ region: row.region || '',++ reliability: row.reliability,++ note: row.note || '',++ variant: row.variant || '',++ source: row.source,++ observed_at: row.observed_at,++ stale: source ? source.stale : true,++ });++ }++ const best = (readings, kind) => {++ const of = readings.filter((r) => r.kind === kind).sort((a, b) => a.hourly_usd - b.hourly_usd);++ const fresh = of.filter((r) => !r.stale);++ return (fresh.length ? fresh : of)[0] || null;++ };++ const gpus = TARGETS.map((target) => {++ const readings = byTarget.get(target.key);++ return {++ key: target.key, gpu: target.gpu, vram_mb: target.vram_mb,++ on_demand: best(readings, 'on_demand'),++ spot: best(readings, 'spot'),++ readings,++ };++ });++ const readAt = sources.map((s) => s.ok_at).filter(Boolean).sort().pop() || null;++ return { read_at: readAt, stale_after_ms: settings.staleMs, sources, gpus };++ }++++ /** The book is due when the last answered read is older than the interval (a deploy does not reset it). */++ async function due() {++ const row = await db.prepare('SELECT MAX(ok_at) AS at FROM gpu_market_sources').get().catch(() => null);++ const at = row && row.at ? Date.parse(row.at) : NaN;++ return !Number.isFinite(at) || clock.now() - at >= settings.intervalMs;++ }++++ function start() {++ if (!settings.enabled || timer) return;++ const tick = async () => {++ if (busy) return;++ busy = true;++ try {++ if (await due()) await refresh();++ } catch (err) {++ log.warn(`[ai] GPU market prices failed: ${String(err && err.message || err).slice(0, 200)}`);++ } finally { busy = false; }++ };++ timer = setInterval(tick, Math.max(60000, settings.tickMs));++ if (timer.unref) timer.unref();++ first = setTimeout(tick, Math.max(1000, settings.firstTickMs));++ if (first.unref) first.unref();++ }++++ function stop() {++ if (timer) clearInterval(timer);++ if (first) clearTimeout(first);++ timer = null;++ first = null;++ }++++ return { refresh, report, due, start, stop, settings, SOURCES, TARGETS };++}++++module.exports = {++ createGpuMarket, parseVast, parseRunpod, parseSalad, parseTensordock, parseCloudrift, targetFor, lowest,++ TARGETS, SOURCES, SOURCE_BY_ID, HOUR_MS,++};nnew file mode 100644@@@ -0,0 +1,126 @@++'use strict';++/**++ * What a catalogued model costs per token on a rented GPU.++ *++ * hourly price from the public marketplace readings (server/gpu/market.js)++ * × 1,000,000 / the model's tokens per hour (the catalog's planning estimate)++ * = USD per million output tokens++ *++ * Beside it is the cheapest published API price for the same or a comparable model, so the owner can see++ * whether the rent is cheaper than buying the tokens. The comparison is deliberately narrow: the exact model++ * first, then the same family (with the same uncensored/standard weights when both say), then any priced model++ * with the same weights. If nothing comparable is priced, there is no comparison rather than a misleading one.++ */++const { list: catalogModels } = require('./catalog');++++const THROUGHPUT_SOURCE = 'catalog planning estimate (server/gpu/catalog.js)';++++function number(value) {++ if (value === null || value === undefined || value === '') return null;++ const n = Number(value);++ return Number.isFinite(n) ? n : null;++}++++function round(value, digits = 6) {++ const factor = 10 ** digits;++ return Math.round(value * factor) / factor;++}++++function nameOf(value) {++ return String(value == null ? '' : value).trim().toUpperCase();++}++++/**++ * The cheapest priced API model comparable with one catalog model.++ * → `{ ...model, match }` or null.++ */++function cheapestComparable(spec, models = []) {++ const sameKeys = new Set([spec.key, spec.model_key, ...(Array.isArray(spec.api_comparables) ? spec.api_comparables : [])].filter(Boolean));++ const family = spec.family ? String(spec.family).toLowerCase() : null;++ const uncensored = spec.uncensored === true || spec.uncensored === false ? Boolean(spec.uncensored) : null;++ const ranked = [];++ for (const model of models) {++ if (!model || String(model.provider_key || '').startsWith('gpu.')) continue;++ const out = number(model.cost && model.cost.out_per_mtok);++ if (out === null || out < 0) continue;++ const modelFamily = model.family ? String(model.family).toLowerCase() : null;++ const modelUncensored = model.uncensored === true || model.uncensored === false ? Boolean(model.uncensored) : null;++ let match = null;++ let score = 4;++ if (sameKeys.has(model.model_key)) { match = 'same_model'; score = 0; }++ else if (family && modelFamily === family) {++ if (uncensored !== null && modelUncensored === uncensored) { match = 'same_family_weights'; score = 1; }++ else { match = 'same_family'; score = 2; }++ }++ else if (uncensored === true && modelUncensored === true) { match = 'uncensored'; score = 3; }++ if (score === 4) continue;++ ranked.push({ ...model, match, score, out_per_mtok: out, in_per_mtok: number(model.cost && model.cost.in_per_mtok) });++ }++ ranked.sort((a, b) => a.score - b.score || a.out_per_mtok - b.out_per_mtok || String(a.model_key).localeCompare(String(b.model_key)));++ const best = ranked[0];++ if (!best) return null;++ return {++ model_key: best.model_key,++ display_name: best.display_name || best.model_key,++ provider_key: best.provider_key,++ family: best.family || null,++ uncensored: best.uncensored === true || best.uncensored === false ? Boolean(best.uncensored) : null,++ in_per_mtok: best.in_per_mtok,++ out_per_mtok: best.out_per_mtok,++ usd_per_million_output_tokens: best.out_per_mtok,++ usd_per_million_input_tokens: best.in_per_mtok,++ match: best.match,++ source: (best.cost && best.cost.source) || best.source || null,++ };++}++++/**++ * One row per catalog model, GPU it fits, kind of price (on demand and spot) and provider reading, so the++ * owner sees the Vast 4090 hour beside the Salad 4090 hour. `is_cheapest` marks the fresh floor for that++ * card and kind. `market` is the report from server/gpu/market.js.++ */++function tokenCosts({ market = null, models = [], catalog = null } = {}) {++ if (!market || !Array.isArray(market.gpus)) return [];++ const catalogList = Array.isArray(catalog) ? catalog : (catalog && typeof catalog.list === 'function' ? catalog.list() : catalogModels());++ const gpus = new Map(market.gpus.map((gpu) => [nameOf(gpu.gpu), gpu]));++ const out = [];++ for (const spec of catalogList) {++ const fits = ((spec.requirements && spec.requirements.gpu_names) || []).map(nameOf).filter((name) => gpus.has(name));++ const throughput = number(spec.throughput_tokens_per_hour);++ if (!Number.isFinite(throughput) || throughput <= 0) continue;++ for (const gpuName of fits) {++ const gpu = gpus.get(gpuName);++ const readings = Array.isArray(gpu.readings) ? gpu.readings : [];++ for (const kind of ['on_demand', 'spot']) {++ const best = gpu[kind];++ for (const reading of readings.filter((r) => r.kind === kind)) {++ const hourly = number(reading.hourly_usd);++ if (!Number.isFinite(hourly) || hourly <= 0) continue;++ out.push({++ model_key: spec.key,++ model_display_name: spec.display_name || spec.key,++ gpu: gpu.gpu,++ gpu_key: gpu.key,++ kind,++ provider: reading.provider,++ provider_label: reading.provider_label || reading.provider,++ region: reading.region || '',++ hourly_usd: hourly,++ reliability: reading.reliability == null ? null : number(reading.reliability),++ observed_at: reading.observed_at,++ stale: Boolean(reading.stale),++ is_cheapest: Boolean(!reading.stale && best && best.provider === reading.provider && best.hourly_usd === reading.hourly_usd && best.observed_at === reading.observed_at),++ source: reading.source,++ throughput_tokens_per_hour: throughput,++ throughput_source: THROUGHPUT_SOURCE,++ usd_per_million_tokens: round((hourly * 1e6) / throughput),++ cheapest_api: cheapestComparable(spec, models),++ });++ }++ }++ }++ }++ return out;++}++++module.exports = { tokenCosts, cheapestComparable, THROUGHPUT_SOURCE };@@@ -57,6 +57,9 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {// API key and a monthly cap in the owner settings (ai.gpus). The reconciler rents, health-checks and// scales the instances to zero; it never spends past the cap.const gpus = require('./gpu/provisioner').createGpuProvisioner({ db, registry, settings, clock, fetchImpl, env, log });++ // The public GPU marketplace prices (server/gpu/market.js): a due-based public-page reader for the console's++ // supply view. It never signs in or rents; a failed source keeps its last reading, marked stale.++ const gpuMarket = require('./gpu/market').createGpuMarket({ db, config, clock, fetchImpl, log });const vault = require('./vault').createVault({ settings, clock, log });await require('./vault').migrate({ db, config, settings, log }).catch((err) => log.warn(`[Vault] migration failed: ${err.message}`));await vault.load().catch((err) => log.warn(`[Vault] ${err.message}`));@@@ -205,7 +208,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 });++ 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 });let jwks = null;let keyLoaded = Promise.resolve(null);if (!keys.pinned && config.nodeEnv !== 'test') {@@@ -259,6 +262,8 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {if (listen && config.nodeEnv !== 'test' && memorySharing) memorySharing.start();// The price book reads the providers' listings once a week; it never starts in test mode or under a drill.if (listen && config.priceBook && config.priceBook.enabled) priceBook.start();++ // The public GPU price reader: public pages only, due-based, off in tests and under a drill.++ if (listen && config.gpuMarket && config.gpuMarket.enabled) gpuMarket.start();// The refusal job asks the offers the same questions once a week; it never starts in test mode (the// tests drive it by hand) and never under a restore drill.if (listen && config.refusal && config.refusal.enabled) refusal.start();@@@ -305,6 +310,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {memoryJobs.stop();if (memorySharing) memorySharing.stop();priceBook.stop();++ gpuMarket.stop();refusal.stop();batches.stop();batcher.stop();@@@ -324,7 +330,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {if (!givenDb) await db.close();}-- return { config, db, registry, pool, quotas, tiers, cache, fetcher, engine, runs, keys, keyLoaded, auth, app, server, bench, benchEvents, memory, memoryJobs, memoryServices, memorySharing, priceBook, refusal, batches, batcher, credits: creditsRef, settings, vault, community, endpoints, webhooks, gpus, abuse, ownEndpoints, close };++ return { config, db, registry, pool, quotas, tiers, cache, fetcher, engine, runs, keys, keyLoaded, auth, app, server, bench, benchEvents, memory, memoryJobs, memoryServices, memorySharing, priceBook, refusal, batches, batcher, credits: creditsRef, settings, vault, community, endpoints, webhooks, gpus, gpuMarket, abuse, ownEndpoints, close };}if (require.main === module) {@@@ -9,10 +9,16 @@ const { SOURCES } = require('../server/pricebook/sources');const { PROVIDERS } = require('../server/providers/pool-providers');const { boot, request, suite } = require('./helpers');const dev = require('./fixtures/market/models-dev.json');--const files = { deepseek:'deepseek.md', qwen:'qwen.html', kimi:'kimi.html', glm:'glm.md', minimax:'minimax.md', deepinfra:'deepinfra.json', novita:'novita.json', featherless:'featherless.json', refuseless:'refuseless.md', abliteration:'abliteration.md' };++// Each own source saves its page under its own key; discover the extension so a new source does not break the replay.++const fixtureFor = (key) => {++ const dir = path.join(__dirname, 'fixtures/pricebook');++ const name = fs.readdirSync(dir).find((entry) => entry.startsWith(key + '.'));++ if (!name) throw new Error(`no saved pricebook page for ${key}`);++ return name;++};const observations = [];for (const s of SOURCES.filter(s => s.role === 'own')) {-- const raw = fs.readFileSync(path.join(__dirname, 'fixtures/pricebook', files[s.key] || `${s.key}.json`), 'utf8');++ const raw = fs.readFileSync(path.join(__dirname, 'fixtures/pricebook', fixtureFor(s.key)), 'utf8');for (const [model_key, price] of Object.entries(s.parse(s.kind === 'json' ? JSON.parse(raw) : raw))) observations.push({ provider_key: s.provider, model_key, price, source: s.url, checked_at: '2026-11-09' });}const t = suite('exchange-market'); let h;nnew file mode 100644@@@ -0,0 +1,7 @@++<!doctype html><html><body>++<p>RTX 4090 NVIDIA On‑Demand $0.45 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>++<p>RTX 5090 NVIDIA On‑Demand $0.65 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>++<p>H100 NVIDIA On‑Demand Upon request 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>++<p>H200 NVIDIA On‑Demand Upon request 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>++<p>A100 SXM4 NVIDIA On‑Demand Upon request 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>++</body></html>nnew file mode 100644@@@ -0,0 +1,11 @@++<!doctype html><html><body>++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H200</p><div class="gpu_table-pricing"><div data-secure="5.29" data-community="3.59" class="gpu_table-price"><span class="active-price">5.29</span></div></div></div></div>++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 NVL</p><div class="gpu_table-pricing"><div data-secure="3.19" data-community="2.59" class="gpu_table-price"><span class="active-price">3.19</span></div></div></div></div>++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 PCIe</p><div class="gpu_table-pricing"><div data-secure="2.89" data-community="1.99" class="gpu_table-price"><span class="active-price">2.89</span></div></div></div></div>++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 SXM</p><div class="gpu_table-pricing"><div data-secure="3.99" data-community="2.69" class="gpu_table-price"><span class="active-price">3.99</span></div></div></div></div>++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">A100 PCIe</p><div class="gpu_table-pricing"><div data-secure="1.79" data-community="1.19" class="gpu_table-price"><span class="active-price">1.79</span></div></div></div></div>++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">A100 SXM</p><div class="gpu_table-pricing"><div data-secure="1.79" data-community="1.39" class="gpu_table-price"><span class="active-price">1.79</span></div></div></div></div>++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 5090</p><div class="gpu_table-pricing"><div data-secure="1.19" data-community="0.69" class="gpu_table-price"><span class="active-price">1.19</span></div></div></div></div>++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 3090</p><div class="gpu_table-pricing"><div data-secure="0.5" data-community="0.22" class="gpu_table-price"><span class="active-price">0.5</span></div></div></div></div>++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 4090</p><div class="gpu_table-pricing"><div data-secure="0.89" data-community="0.34" class="gpu_table-price"><span class="active-price">0.89</span></div></div></div></div>++</body></html>nnew file mode 100644@@@ -0,0 +1 @@++<!doctype html><html><body><script type=application/json id=gpu-prices>[{"name":"RTX 5090","vram_gb":32,"high":0.6,"medium":0.483,"low":0.367,"lowest":0.25},{"name":"RTX 4090","vram_gb":24,"high":0.44,"medium":0.347,"low":0.253,"lowest":0.16},{"name":"RTX 5080","vram_gb":16,"high":0.32,"medium":0.263,"low":0.207,"lowest":0.15},{"name":"RTX 5070 Ti","vram_gb":16,"high":0.27,"medium":0.213,"low":0.157,"lowest":0.1},{"name":"RTX 4080","vram_gb":16,"high":0.27,"medium":0.217,"low":0.163,"lowest":0.11},{"name":"RTX 4070 Ti Super","vram_gb":16,"high":0.24,"medium":0.19,"low":0.14,"lowest":0.09},{"name":"RTX 5090 Laptop","vram_gb":24,"high":0.235,"medium":0.19,"low":0.145,"lowest":0.1},{"name":"RTX 3090 Ti","vram_gb":24,"high":0.22,"medium":0.18,"low":0.14,"lowest":0.1},{"name":"RTX 3090","vram_gb":24,"high":0.2,"medium":0.163,"low":0.127,"lowest":0.09},{"name":"RTX 5070","vram_gb":12,"high":0.2,"medium":0.16,"low":0.12,"lowest":0.08},{"name":"RTX 4070 Ti","vram_gb":12,"high":0.2,"medium":0.16,"low":0.12,"lowest":0.08},{"name":"RTX 3080 Ti","vram_gb":12,"high":0.195,"medium":0.157,"low":0.118,"lowest":0.08},{"name":"RTX 4070","vram_gb":12,"high":0.165,"medium":0.133,"low":0.102,"lowest":0.07},{"name":"RTX 5060 Ti","vram_gb":16,"high":0.155,"medium":0.127,"low":0.098,"lowest":0.07},{"name":"RTX 3080","vram_gb":10,"high":0.155,"medium":0.123,"low":0.092,"lowest":0.06},{"name":"RTX 4060 Ti","vram_gb":16,"high":0.14,"medium":0.12,"low":0.1,"lowest":0.08},{"name":"RTX 5060","vram_gb":8,"high":0.115,"medium":0.098,"low":0.082,"lowest":0.065}]</script></body></html>nnew file mode 100644@@@ -0,0 +1,6 @@++<!doctype html><html><body>++<h2>Our most popular GPUs</h2>++<p>H100 SXM5 From $2.25/hr For builders needing training and inference with no compromises.</p>++<p>A100 SXM4 From $1.80/hr The best balance between price and performance for AI inference.</p>++<p>Take advantage of our consumer GPUs from $0.12/hr for up to 5x inference value.</p>++</body></html>nnew file mode 100644@@@ -0,0 +1,754 @@++{++ "offers": [++ {++ "id": 20130142,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.08148148148148149,++ "min_bid": 0.08,++ "reliability": 0.9933032,++ "reliability2": 0.9933032,++ "geolocation": "Czechia, CZ",++ "verification": "verified",++ "storage_cost": 0.1333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 38849861,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.09555555555555556,++ "min_bid": 0.09333333333333334,++ "reliability": 0.9942635,++ "reliability2": 0.9942635,++ "geolocation": "Quebec, CA",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59612506,++ "gpu_name": "RTX 4090",++ "gpu_ram": 16376,++ "num_gpus": 1,++ "dph_total": 0.13629626296296296,++ "min_bid": 0.1333333,++ "reliability": 0.9353087,++ "reliability2": 0.9353087,++ "geolocation": "California, US",++ "verification": "unverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59373283,++ "gpu_name": "A100 PCIE",++ "gpu_ram": 40960,++ "num_gpus": 1,++ "dph_total": 0.1355555222222222,++ "min_bid": 0.1333333,++ "reliability": 0.9681284,++ "reliability2": 0.9681284,++ "geolocation": "Beijing, CN",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 40436852,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.1348148148148148,++ "min_bid": 0.13333333333333333,++ "reliability": 0.9983809,++ "reliability2": 0.9983809,++ "geolocation": "Greece, GR",++ "verification": "unverified",++ "storage_cost": 0.1333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59874081,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.13555555555555554,++ "min_bid": 0.13333333333333333,++ "reliability": 0.9952501,++ "reliability2": 0.9952501,++ "geolocation": "Montana, US",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 30714840,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.14888892222222222,++ "min_bid": 0.1466667,++ "reliability": 0.9476724,++ "reliability2": 0.9476724,++ "geolocation": "The Netherlands, NL",++ "verification": "verified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 57970526,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.16407407407407407,++ "min_bid": 0.16,++ "reliability": 0.9876611,++ "reliability2": 0.9876611,++ "geolocation": "Quebec, CA",++ "verification": "deverified",++ "storage_cost": 0.3666666666666667,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 47563804,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.17037037037037037,++ "min_bid": 0.16,++ "reliability": 0.9741023,++ "reliability2": 0.9741023,++ "geolocation": "Quebec, CA",++ "verification": "verified",++ "storage_cost": 0.9333333333333335,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 27177907,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.1648888888888889,++ "min_bid": 0.16,++ "reliability": 0.9868965,++ "reliability2": 0.9868965,++ "geolocation": "Czechia, CZ",++ "verification": "deverified",++ "storage_cost": 0.44,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58273870,++ "gpu_name": "A100 PCIE",++ "gpu_ram": 81920,++ "num_gpus": 1,++ "dph_total": 0.16822222222222222,++ "min_bid": 0.166,++ "reliability": 0.8620362,++ "reliability2": 0.8620362,++ "geolocation": "United Kingdom, GB",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 57810853,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.19555555555555554,++ "min_bid": 0.19333333333333333,++ "reliability": 0.965621,++ "reliability2": 0.965621,++ "geolocation": "Thailand, TH",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58831870,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.2022222222222222,++ "min_bid": 0.19999999999999998,++ "reliability": 0.9988663,++ "reliability2": 0.9988663,++ "geolocation": "Quebec, CA",++ "verification": "verified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 56007282,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.20222222222222222,++ "min_bid": 0.2,++ "reliability": 0.9974512,++ "reliability2": 0.9974512,++ "geolocation": "Nevada, US",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 56141038,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.21629629629629632,++ "min_bid": 0.21333333333333335,++ "reliability": 0.9533946,++ "reliability2": 0.9533946,++ "geolocation": "Quebec, CA",++ "verification": "deverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 9817760,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.22165925925925925,++ "min_bid": 0.22,++ "reliability": 0.9991736,++ "reliability2": 0.9991736,++ "geolocation": "Ireland, IE",++ "verification": "deverified",++ "storage_cost": 0.14933333333333335,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59877086,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.2277037037037037,++ "min_bid": 0.224,++ "reliability": 0.9731478,++ "reliability2": 0.9731478,++ "geolocation": "Vietnam, VN",++ "verification": "verified",++ "storage_cost": 0.3333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58120176,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.23296296296296298,++ "min_bid": 0.23,++ "reliability": 0.994813,++ "reliability2": 0.994813,++ "geolocation": "Texas, US",++ "verification": "deverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 53074074,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.2688888888888889,++ "min_bid": 0.26666666666666666,++ "reliability": 0.9511167,++ "reliability2": 0.9511167,++ "geolocation": "Texas, US",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 54080523,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.2688889222222222,++ "min_bid": 0.2666667,++ "reliability": 0.9659244,++ "reliability2": 0.9659244,++ "geolocation": "Texas, US",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 50687676,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.2696296629629629,++ "min_bid": 0.2666667,++ "reliability": 0.9831286,++ "reliability2": 0.9831286,++ "geolocation": "New Jersey, US",++ "verification": "unverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 56202921,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.28222222222222226,++ "min_bid": 0.28,++ "reliability": 0.9928783,++ "reliability2": 0.9928783,++ "geolocation": "Hong Kong, HK",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58523997,++ "gpu_name": "A100 SXM4",++ "gpu_ram": 40960,++ "num_gpus": 1,++ "dph_total": 0.29555555555555557,++ "min_bid": 0.29333333333333333,++ "reliability": 0.9770653,++ "reliability2": 0.9770653,++ "geolocation": "Iowa, US",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58955859,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.32296296296296295,++ "min_bid": 0.32,++ "reliability": 0.9625633,++ "reliability2": 0.9625633,++ "geolocation": "Quebec, CA",++ "verification": "deverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 37930682,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.3251851851851852,++ "min_bid": 0.32,++ "reliability": 0.9977738,++ "reliability2": 0.9977738,++ "geolocation": "Spain, ES",++ "verification": "verified",++ "storage_cost": 0.46666666666666673,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58868421,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.33555555555555555,++ "min_bid": 0.3333333333333333,++ "reliability": 0.9786792,++ "reliability2": 0.9786792,++ "geolocation": "United States, US",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59924610,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.3562222222222222,++ "min_bid": 0.354,++ "reliability": 0.9902221,++ "reliability2": 0.9902221,++ "geolocation": "Belgium, BE",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 57801635,++ "gpu_name": "RTX 4090",++ "gpu_ram": 49140,++ "num_gpus": 1,++ "dph_total": 0.3622222222222223,++ "min_bid": 0.36000000000000004,++ "reliability": 0.9880769,++ "reliability2": 0.9880769,++ "geolocation": "Thailand, TH",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 57527864,++ "gpu_name": "A100 PCIE",++ "gpu_ram": 40960,++ "num_gpus": 1,++ "dph_total": 0.3742222222222223,++ "min_bid": 0.37200000000000005,++ "reliability": 0.9643426,++ "reliability2": 0.9643426,++ "geolocation": "Japan, JP",++ "verification": "verified",++ "storage_cost": 0.20000000000000004,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59888567,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.3844148148148148,++ "min_bid": 0.37333333333333335,++ "reliability": 0.9968028,++ "reliability2": 0.9968028,++ "geolocation": "Taiwan, TW",++ "verification": "verified",++ "storage_cost": 0.9973333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 36643053,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.3925925925925926,++ "min_bid": 0.38666666666666666,++ "reliability": 0.9871033,++ "reliability2": 0.9871033,++ "geolocation": "United Kingdom, GB",++ "verification": "deverified",++ "storage_cost": 0.5333333333333332,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 25814725,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.39155555555555555,++ "min_bid": 0.3893333333333333,++ "reliability": 0.9976939,++ "reliability2": 0.9976939,++ "geolocation": "Iceland, IS",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59846906,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.40962962962962957,++ "min_bid": 0.39999999999999997,++ "reliability": 0.9845932,++ "reliability2": 0.9845932,++ "geolocation": "Beijing, CN",++ "verification": "deverified",++ "storage_cost": 0.8666666666666665,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59137888,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.40222222222222226,++ "min_bid": 0.4,++ "reliability": 0.9954978,++ "reliability2": 0.9954978,++ "geolocation": "Alberta, CA",++ "verification": "verified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 50284377,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.41555555555555557,++ "min_bid": 0.41333333333333333,++ "reliability": 0.9116739,++ "reliability2": 0.9116739,++ "geolocation": "Minnesota, US",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59857072,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.4330074074074074,++ "min_bid": 0.43226666666666663,++ "reliability": 0.9637782,++ "reliability2": 0.9637782,++ "geolocation": "United States, US",++ "verification": "deverified",++ "storage_cost": 0.06666666666666665,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 38116562,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.4555555555555556,++ "min_bid": 0.45333333333333337,++ "reliability": 0.9976416,++ "reliability2": 0.9976416,++ "geolocation": "British Columbia, CA",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 51341862,++ "gpu_name": "A100 SXM4",++ "gpu_ram": 40960,++ "num_gpus": 1,++ "dph_total": 0.4718518518518518,++ "min_bid": 0.4666666666666666,++ "reliability": 0.7979845,++ "reliability2": 0.7979845,++ "geolocation": "Mississippi, US",++ "verification": "verified",++ "storage_cost": 0.46666666666666673,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58940647,++ "gpu_name": "A100 SXM4",++ "gpu_ram": 81920,++ "num_gpus": 1,++ "dph_total": 0.5222222222222223,++ "min_bid": 0.52,++ "reliability": 0.9847828,++ "reliability2": 0.9847828,++ "geolocation": "Austria, AT",++ "verification": "verified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59830761,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.5222222222222223,++ "min_bid": 0.52,++ "reliability": 0.9810733,++ "reliability2": 0.9810733,++ "geolocation": "Guangdong, CN",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 57376615,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.5348148148148149,++ "min_bid": 0.5333333333333333,++ "reliability": 0.9799656,++ "reliability2": 0.9799656,++ "geolocation": "Taiwan, TW",++ "verification": "deverified",++ "storage_cost": 0.1333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58667289,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.5355555555555556,++ "min_bid": 0.5333333333333333,++ "reliability": 0.9898541,++ "reliability2": 0.9898541,++ "geolocation": "Thailand, TH",++ "verification": "verified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 50734486,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.6022222222222222,++ "min_bid": 0.6,++ "reliability": 0.9924844,++ "reliability2": 0.9924844,++ "geolocation": "South Korea, KR",++ "verification": "verified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59227354,++ "gpu_name": "A100 SXM4",++ "gpu_ram": 81920,++ "num_gpus": 1,++ "dph_total": 0.6586222222222222,++ "min_bid": 0.6564,++ "reliability": 0.9961462,++ "reliability2": 0.9961462,++ "geolocation": "United Arab Emirates, AE",++ "verification": "unverified",++ "storage_cost": 0.20000000000000004,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59796716,++ "gpu_name": "RTX 4090",++ "gpu_ram": 49140,++ "num_gpus": 1,++ "dph_total": 0.6703703703703703,++ "min_bid": 0.6666666666666666,++ "reliability": 0.9982841,++ "reliability2": 0.9982841,++ "geolocation": "Romania, RO",++ "verification": "verified",++ "storage_cost": 0.3333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 48704103,++ "gpu_name": "H100 SXM",++ "gpu_ram": 81559,++ "num_gpus": 1,++ "dph_total": 0.6740444444444444,++ "min_bid": 0.6666666666666666,++ "reliability": 0.9936286,++ "reliability2": 0.9936286,++ "geolocation": "Czechia, CZ",++ "verification": "verified",++ "storage_cost": 0.664,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59391499,++ "gpu_name": "H100 PCIE",++ "gpu_ram": 81559,++ "num_gpus": 1,++ "dph_total": 0.6743731555555555,++ "min_bid": 0.6666666666666666,++ "reliability": 0.9657309,++ "reliability2": 0.9657309,++ "geolocation": "South Africa, ZA",++ "verification": "verified",++ "storage_cost": 0.6935840000000001,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 56885480,++ "gpu_name": "H100 PCIE",++ "gpu_ram": 81559,++ "num_gpus": 1,++ "dph_total": 0.6707562518518518,++ "min_bid": 0.6666666666666666,++ "reliability": 0.9879379,++ "reliability2": 0.9879379,++ "geolocation": "South Africa, ZA",++ "verification": "verified",++ "storage_cost": 0.36806266666666665,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 30055164,++ "gpu_name": "RTX 4090",++ "gpu_ram": 49140,++ "num_gpus": 1,++ "dph_total": 0.7355555555555556,++ "min_bid": 0.7333333333333334,++ "reliability": 0.998608,++ "reliability2": 0.998608,++ "geolocation": "United Kingdom, GB",++ "verification": "verified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59631467,++ "gpu_name": "A100 SXM4",++ "gpu_ram": 40960,++ "num_gpus": 1,++ "dph_total": 0.7355555555555556,++ "min_bid": 0.7333333333333334,++ "reliability": 0.9452575,++ "reliability2": 0.9452575,++ "geolocation": "California, US",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,604 @@++{++ "offers": [++ {++ "id": 56919557,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.09481481481481482,++ "min_bid": 0.09333333333333334,++ "reliability": 0.9734428,++ "reliability2": 0.9734428,++ "geolocation": "Ukraine, UA",++ "verification": "unverified",++ "storage_cost": 0.1333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 38849861,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.12222222222222223,++ "min_bid": 0.09333333333333334,++ "reliability": 0.9942635,++ "reliability2": 0.9942635,++ "geolocation": "Quebec, CA",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 55934598,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.13590148148148148,++ "min_bid": 0.19999999999999998,++ "reliability": 0.8926473,++ "reliability2": 0.8926473,++ "geolocation": "Texas, US",++ "verification": "deverified",++ "storage_cost": 0.1333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 35376208,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.14888888888888888,++ "min_bid": 0.14666666666666667,++ "reliability": 0.9747085,++ "reliability2": 0.9747085,++ "geolocation": "Guangxi Zhuangzu, CN",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59878525,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.17555555555555555,++ "min_bid": 0.16,++ "reliability": 0.7877959,++ "reliability2": 0.7877959,++ "geolocation": "California, US",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 57314311,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.19074074074074074,++ "min_bid": 0.16,++ "reliability": 0.9977229,++ "reliability2": 0.9977229,++ "geolocation": "Quebec, CA",++ "verification": "deverified",++ "storage_cost": 0.3666666666666667,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58393573,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.2022222222222222,++ "min_bid": 0.09333333,++ "reliability": 0.9888245,++ "reliability2": 0.9888245,++ "geolocation": "Kuwait, KW",++ "verification": "verified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 36757236,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.20414814814814813,++ "min_bid": 0.2,++ "reliability": 0.99256,++ "reliability2": 0.99256,++ "geolocation": "Bulgaria, BG",++ "verification": "verified",++ "storage_cost": 0.1333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 19602471,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.21288888888888888,++ "min_bid": 0.235,++ "reliability": 0.9969887,++ "reliability2": 0.9969887,++ "geolocation": "South Korea, KR",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 55957532,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.21629629629629632,++ "min_bid": 0.16,++ "reliability": 0.9938383,++ "reliability2": 0.9938383,++ "geolocation": "Quebec, CA",++ "verification": "deverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 9817760,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.23232592592592594,++ "min_bid": 0.22,++ "reliability": 0.9991736,++ "reliability2": 0.9991736,++ "geolocation": "Ireland, IE",++ "verification": "deverified",++ "storage_cost": 0.14933333333333335,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 55359923,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.32222222222222224,++ "min_bid": 0.27999999999999997,++ "reliability": 0.9899092,++ "reliability2": 0.9899092,++ "geolocation": "Hong Kong, HK",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59612506,++ "gpu_name": "RTX 4090",++ "gpu_ram": 16376,++ "num_gpus": 1,++ "dph_total": 0.33629629629629626,++ "min_bid": 0.1333333,++ "reliability": 0.9353087,++ "reliability2": 0.9353087,++ "geolocation": "California, US",++ "verification": "unverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 46773554,++ "gpu_name": "A100 PCIE",++ "gpu_ram": 40960,++ "num_gpus": 1,++ "dph_total": 0.337037037037037,++ "min_bid": 0.3333333333333333,++ "reliability": 0.9963616,++ "reliability2": 0.9963616,++ "geolocation": "Japan, JP",++ "verification": "deverified",++ "storage_cost": 0.3333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59554534,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.36888888888888893,++ "min_bid": 0.13333333333333333,++ "reliability": 0.9769943,++ "reliability2": 0.9769943,++ "geolocation": "Czechia, CZ",++ "verification": "verified",++ "storage_cost": 0.20000000000000004,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59877086,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.3743703703703704,++ "min_bid": 0.224,++ "reliability": 0.9731478,++ "reliability2": 0.9731478,++ "geolocation": "Vietnam, VN",++ "verification": "verified",++ "storage_cost": 0.3333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 37930682,++ "gpu_name": "RTX 3090",++ "gpu_ram": 24576,++ "num_gpus": 1,++ "dph_total": 0.37851851851851853,++ "min_bid": 0.32,++ "reliability": 0.9977738,++ "reliability2": 0.9977738,++ "geolocation": "Spain, ES",++ "verification": "verified",++ "storage_cost": 0.46666666666666673,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 25814727,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.4022222222222222,++ "min_bid": 0.3893333333333333,++ "reliability": 0.9976939,++ "reliability2": 0.9976939,++ "geolocation": "Iceland, IS",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 56063498,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.4022222222222222,++ "min_bid": 0.39999999999999997,++ "reliability": 0.9914165,++ "reliability2": 0.9914165,++ "geolocation": "New York, US",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 57004718,++ "gpu_name": "A100 SXM4",++ "gpu_ram": 40960,++ "num_gpus": 1,++ "dph_total": 0.4029629629629629,++ "min_bid": 0.39999999999999997,++ "reliability": 0.9873088,++ "reliability2": 0.9873088,++ "geolocation": "California, US",++ "verification": "deverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 56141036,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.44296296296296295,++ "min_bid": 0.21333333333333335,++ "reliability": 0.9533946,++ "reliability2": 0.9533946,++ "geolocation": "Quebec, CA",++ "verification": "deverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59749210,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.4562962962962963,++ "min_bid": 0.45333333333333337,++ "reliability": 0.8993912,++ "reliability2": 0.8993912,++ "geolocation": "The Netherlands, NL",++ "verification": "unverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59884402,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.462962962962963,++ "min_bid": 0.39999999999999997,++ "reliability": 0.9903628,++ "reliability2": 0.9903628,++ "geolocation": "Beijing, CN",++ "verification": "deverified",++ "storage_cost": 0.8666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 38116562,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.46888888888888886,++ "min_bid": 0.45333333333333337,++ "reliability": 0.9976416,++ "reliability2": 0.9976416,++ "geolocation": "British Columbia, CA",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59808361,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.47111111111111104,++ "min_bid": 0.4666666666666666,++ "reliability": 0.9959959,++ "reliability2": 0.9959959,++ "geolocation": "Arizona, US",++ "verification": "verified",++ "storage_cost": 0.39999999999999997,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 36643053,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.47259259259259256,++ "min_bid": 0.38666666666666666,++ "reliability": 0.9871033,++ "reliability2": 0.9871033,++ "geolocation": "United Kingdom, GB",++ "verification": "deverified",++ "storage_cost": 0.5333333333333332,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58523997,++ "gpu_name": "A100 SXM4",++ "gpu_ram": 40960,++ "num_gpus": 1,++ "dph_total": 0.4822222222222223,++ "min_bid": 0.29333333333333333,++ "reliability": 0.9770653,++ "reliability2": 0.9770653,++ "geolocation": "Iowa, US",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 50103798,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.48414814814814816,++ "min_bid": 0.496,++ "reliability": 0.9960525,++ "reliability2": 0.9960525,++ "geolocation": "Texas, US",++ "verification": "deverified",++ "storage_cost": 0.1333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 48882881,++ "gpu_name": "RTX 4090",++ "gpu_ram": 49140,++ "num_gpus": 1,++ "dph_total": 0.48888888888888893,++ "min_bid": 0.39999999999999997,++ "reliability": 0.9914976,++ "reliability2": 0.9914976,++ "geolocation": "California, US",++ "verification": "verified",++ "storage_cost": 0.7999999999999999,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58382376,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.497037037037037,++ "min_bid": 0.6666666666666666,++ "reliability": 0.9884196,++ "reliability2": 0.9884196,++ "geolocation": "South Korea, KR",++ "verification": "verified",++ "storage_cost": 0.3333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 50967859,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.497037037037037,++ "min_bid": 0.6666666666666666,++ "reliability": 0.9906751,++ "reliability2": 0.9906751,++ "geolocation": "South Korea, KR",++ "verification": "verified",++ "storage_cost": 0.3333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 57994723,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.5044444444444444,++ "min_bid": 0.49333333333333335,++ "reliability": 0.9870163,++ "reliability2": 0.9870163,++ "geolocation": "Poland, PL",++ "verification": "unverified",++ "storage_cost": 1,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59618563,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.5125925925925925,++ "min_bid": 0.326,++ "reliability": 0.9823852,++ "reliability2": 0.9823852,++ "geolocation": "California, US",++ "verification": "verified",++ "storage_cost": 0.5333333333333332,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58803461,++ "gpu_name": "A100 PCIE",++ "gpu_ram": 40960,++ "num_gpus": 1,++ "dph_total": 0.5355555555555556,++ "min_bid": 0.5333333333333333,++ "reliability": 0.9601313,++ "reliability2": 0.9601313,++ "geolocation": "Japan, JP",++ "verification": "unverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59830763,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.5355555555555556,++ "min_bid": 0.52,++ "reliability": 0.9810733,++ "reliability2": 0.9810733,++ "geolocation": "Guangdong, CN",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59937771,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.5392592592592592,++ "min_bid": 0.3466666666666667,++ "reliability": 0.9749228,++ "reliability2": 0.9749228,++ "geolocation": "California, US",++ "verification": "unverified",++ "storage_cost": 0.5333333333333332,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 59524409,++ "gpu_name": "RTX 4090",++ "gpu_ram": 49140,++ "num_gpus": 1,++ "dph_total": 0.5484444444444444,++ "min_bid": 0.3333333333333333,++ "reliability": 0.9602438,++ "reliability2": 0.9602438,++ "geolocation": "Sichuan, CN",++ "verification": "unverified",++ "storage_cost": 0.16000000000000003,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 57376611,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.5614814814814816,++ "min_bid": 0.5333333333333333,++ "reliability": 0.9799656,++ "reliability2": 0.9799656,++ "geolocation": "Taiwan, TW",++ "verification": "deverified",++ "storage_cost": 0.1333333333333333,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 58120176,++ "gpu_name": "RTX 4090",++ "gpu_ram": 24564,++ "num_gpus": 1,++ "dph_total": 0.5749629629629629,++ "min_bid": 0.23,++ "reliability": 0.994813,++ "reliability2": 0.994813,++ "geolocation": "Texas, US",++ "verification": "deverified",++ "storage_cost": 0.2666666666666666,++ "is_bid": false,++ "rentable": true++ },++ {++ "id": 53074074,++ "gpu_name": "RTX 5090",++ "gpu_ram": 32607,++ "num_gpus": 1,++ "dph_total": 0.6022222222222222,++ "min_bid": 0.26666666666666666,++ "reliability": 0.9511167,++ "reliability2": 0.9511167,++ "geolocation": "Texas, US",++ "verification": "deverified",++ "storage_cost": 0.19999999999999998,++ "is_bid": false,++ "rentable": true++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,178 @@++'use strict';++// The public GPU marketplace price reader: saved provider pages, the cache that survives a failed source, and++// the per-token cost implied by the catalog's throughput next to the cheapest comparable API price. No test++// reaches the internet: every source is a saved page from test/fixtures/gpu-market/.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { boot, request, token, suite } = require('./helpers');++const { testDb } = require('./db');++const marketLib = require('../server/gpu/market');++const { tokenCosts, cheapestComparable } = require('../server/gpu/token-cost');++const catalog = require('../server/gpu/catalog');++++const FIXTURES = path.join(__dirname, 'fixtures', 'gpu-market');++const fixture = (name) => fs.readFileSync(path.join(FIXTURES, name), 'utf8');++++/** The saved pages, behind the same fetch seam the service uses. Vast is called twice (on demand, then bid). */++function fixtureFetch({ fail = null } = {}) {++ let vastCalls = 0;++ return async (url) => {++ if (fail && String(url).includes(fail)) throw new Error('saved source is down');++ let name;++ if (String(url).includes('vast.ai')) name = vastCalls++ === 0 ? 'vast-ondemand.json' : 'vast-bid.json';++ else if (String(url).includes('runpod.io')) name = 'runpod-pricing.html';++ else if (String(url).includes('salad.com')) name = 'salad-pricing.html';++ else if (String(url).includes('tensordock.com')) name = 'tensordock-pricing.html';++ else if (String(url).includes('cloudrift.ai')) name = 'cloudrift-pricing.html';++ else throw new Error(`no saved fixture for ${url}`);++ const text = fixture(name);++ return { ok: true, status: 200, text: async () => text };++ };++}++++const t = suite('gpu-market');++let h;++let db;++let now = Date.parse('2026-11-09T20:00:00Z');++let market;++let goodReport;++++t.test('the saved provider pages parse into the same reading shape', async () => {++ const onDemand = marketLib.parseVast(fixture('vast-ondemand.json'), 'on_demand');++ const bid = marketLib.parseVast(fixture('vast-bid.json'), 'spot');++ assert.ok(onDemand.length > 0 && bid.length > 0);++ assert.equal(marketLib.targetFor('A100 PCIE', 40960), null, 'a 40 GB A100 is not the 80 GB row');++ assert.equal(marketLib.targetFor('A100 SXM4', 81920).gpu, 'A100 80GB');++ const lowest = (rows, key, kind) => marketLib.lowest(rows).filter((r) => r.gpu_key === key && r.kind === kind)++ .sort((a, b) => a.hourly_usd - b.hourly_usd)[0];++ const vast4090 = lowest(onDemand, 'rtx-4090', 'on_demand');++ assert.equal(vast4090.hourly_usd, 0.13590148148148148);++ assert.equal(vast4090.region, 'Texas, US');++ assert.ok(vast4090.reliability > 0 && vast4090.reliability <= 1);++ assert.match(vast4090.note, /storage/);++ assert.equal(lowest(bid, 'rtx-4090', 'spot').hourly_usd, 0.1466667);++ const offer = { num_gpus: 1, gpu_name: 'RTX 4090', gpu_ram: 24576, dph_total: 1 };++ for (const reliability of [null, undefined, '']) {++ assert.strictEqual(marketLib.parseVast({ offers: [{ ...offer, reliability }] }, 'on_demand')[0].reliability, null);++ }++ assert.strictEqual(marketLib.parseVast({ offers: [{ ...offer, reliability: 0 }] }, 'on_demand')[0].reliability, 0);++++ const runpod = marketLib.lowest(marketLib.parseRunpod(fixture('runpod-pricing.html')));++ assert.equal(runpod.find((r) => r.gpu_key === 'h100' && r.kind === 'on_demand').hourly_usd, 1.99);++ assert.ok(runpod.every((r) => r.kind === 'on_demand'), 'Community Cloud is not a spot quote');++ assert.equal(runpod.find((r) => r.gpu_key === 'rtx-4090' && r.kind === 'on_demand').hourly_usd, 0.34);++++ const salad = marketLib.lowest(marketLib.parseSalad(fixture('salad-pricing.html')));++ assert.equal(salad.find((r) => r.gpu_key === 'rtx-4090' && r.kind === 'on_demand').hourly_usd, 0.44);++ assert.equal(salad.find((r) => r.gpu_key === 'rtx-4090' && r.kind === 'spot').hourly_usd, 0.16);++ assert.match(salad.find((r) => r.gpu_key === 'rtx-4090' && r.kind === 'spot').note, /interruptible/);++++ const tensordock = marketLib.parseTensordock(fixture('tensordock-pricing.html'));++ assert.equal(tensordock.find((r) => r.gpu_key === 'h100').hourly_usd, 2.25);++ assert.equal(tensordock.find((r) => r.gpu_key === 'a100-80gb').hourly_usd, 1.8);++ assert.equal(tensordock.some((r) => r.kind === 'spot'), false, 'TensorDock publishes no spot floor on its public page');++++ const cloudrift = marketLib.parseCloudrift(fixture('cloudrift-pricing.html'));++ assert.equal(cloudrift.find((r) => r.gpu_key === 'rtx-4090').hourly_usd, 0.45);++ assert.equal(cloudrift.find((r) => r.gpu_key === 'rtx-5090').hourly_usd, 0.65);++ assert.equal(cloudrift.some((r) => r.gpu_key === 'h100'), false, 'CloudRift quotes H100 on demand on request');++});++++t.test('the cache keeps the lowest reading per provider, card and kind', async () => {++ db = await testDb();++ market = marketLib.createGpuMarket({ db: db.db, config: { gpuMarket: { enabled: false } }, clock: { now: () => now } });++ const out = await market.refresh({ fetchImpl: fixtureFetch() });++ assert.equal(out.refreshed, 5);++ assert.equal(out.failed, 0);++ goodReport = await market.report();++ assert.equal(goodReport.sources.length, 5);++ assert.ok(goodReport.sources.every((s) => s.status === 'ok' && s.stale === false));++ const g = (key) => goodReport.gpus.find((x) => x.key === key);++ assert.equal(g('rtx-4090').on_demand.provider, 'vast');++ assert.equal(g('rtx-4090').on_demand.hourly_usd, 0.13590148148148148);++ assert.equal(g('rtx-4090').spot.hourly_usd, 0.1466667);++ assert.equal(g('h100').on_demand.provider, 'runpod');++ assert.equal(g('h100').spot.provider, 'vast');++ assert.equal(g('h100').spot.hourly_usd, 0.6666666666666666);++ assert.ok(g('h200').on_demand);++ assert.strictEqual(g('h200').spot, null, 'the source has no H200 spot quote');++ assert.equal(await market.due(), false, 'a fresh read is not due again');++ now += 7 * 60 * 60 * 1000;++ assert.equal(await market.due(), true, 'the six-hour interval makes the next read due');++ now -= 7 * 60 * 60 * 1000;++});++++t.test('a source that fails keeps its last reading, marked stale', async () => {++ const before = goodReport.gpus.find((x) => x.key === 'h200').on_demand;++ const okAt = goodReport.sources.find((s) => s.provider === 'runpod').ok_at;++ now += 10 * 60 * 1000;++ const out = await market.refresh({ fetchImpl: fixtureFetch({ fail: 'runpod.io' }) });++ assert.equal(out.failed, 1);++ assert.equal(out.refreshed, 4);++ const report = await market.report();++ const runpod = report.sources.find((s) => s.provider === 'runpod');++ assert.equal(runpod.status, 'failed');++ assert.equal(runpod.stale, true);++ assert.equal(runpod.ok_at, okAt);++ assert.match(runpod.error, /saved source is down/);++ const h200 = report.gpus.find((x) => x.key === 'h200');++ assert.equal(h200.on_demand.provider, before.provider);++ assert.equal(h200.on_demand.hourly_usd, before.hourly_usd);++ assert.equal(h200.on_demand.stale, true);++ assert.ok(h200.readings.length > 0, 'the last rows were not deleted');++ const stale = structuredClone(goodReport);++ for (const gpu of stale.gpus) for (const reading of gpu.readings) reading.stale = true;++ assert.ok(tokenCosts({ market: stale, catalog }).every((row) => !row.is_cheapest), 'stale readings cannot claim the fresh price floor');++});++++t.test('the implied per-token cost uses the catalog throughput and the cheapest comparable API', async () => {++ const models = [++ { provider_key: 'deepseek', model_key: 'deepseek-v4-flash', display_name: 'DeepSeek V4 Flash', family: 'deepseek', uncensored: false, cost: { in_per_mtok: 0.037, out_per_mtok: 0.17 } },++ { provider_key: 'refuseless', model_key: 'deepseek-v4.1-flash-abliterated', display_name: 'DeepSeek V4.1 Flash (abliterated)', family: 'deepseek', uncensored: true, cost: { in_per_mtok: null, out_per_mtok: 0.5 } },++ { provider_key: 'openai', model_key: 'gpt-5-nano', display_name: 'GPT-5 nano', family: 'gpt', uncensored: false, cost: { in_per_mtok: 0.05, out_per_mtok: 0.4 } },++ ];++ // RunPod is stale from the previous test; the 3090 and 4090 rows are Vast and fresh.++ const rows = tokenCosts({ market: goodReport, models, catalog });++ assert.equal(rows.length, 22, 'both models, both fitting cards, both kinds and every provider reading');++ const row = (key, gpu, kind, provider) => rows.find((r) => r.model_key === key && r.gpu === gpu && r.kind === kind && r.provider === provider);++ const p = row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'vast');++ assert.equal(p.is_cheapest, true);++ assert.ok(row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'salad'), 'the Salad 4090 hour is beside Vast');++ assert.equal(row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'cloudrift').hourly_usd, 0.45);++ assert.equal(row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'cloudrift').is_cheapest, false);++ assert.equal(p.hourly_usd, 0.13590148148148148);++ assert.equal(p.throughput_tokens_per_hour, 180000);++ assert.match(p.throughput_source, /catalog planning estimate/);++ assert.equal(p.usd_per_million_tokens, 0.755008);++ assert.equal(p.cheapest_api.model_key, 'deepseek-v4-flash');++ assert.equal(p.cheapest_api.match, 'same_family_weights');++ const a = row('deepseek-r1-32b-abliterated', 'RTX 4090', 'on_demand', 'vast');++ assert.equal(a.cheapest_api.model_key, 'deepseek-v4.1-flash-abliterated', 'the abliterated comparison prefers the same weights');++ assert.equal(a.cheapest_api.match, 'same_family_weights');++ assert.equal(cheapestComparable({ key: 'a', family: 'none', uncensored: false }, models), null);++});++++t.test('the admin API exposes both to the GPUs and supply console sections', async () => {++ h = await boot();++ await h.gpuMarket.refresh({ fetchImpl: fixtureFetch() });++ const read = token('console', ['ai.usage.read']);++ const gpus = await request(h.base, 'GET', '/api/v1/admin/console/gpus', { tok: read });++ assert.equal(gpus.status, 200, gpus.text);++ assert.equal(gpus.body.market.sources.length, 5);++ assert.equal(gpus.body.token_costs.length, 22);++ assert.ok(gpus.body.state && Array.isArray(gpus.body.state.models));++ const supply = await request(h.base, 'GET', '/api/v1/admin/console/supply', { tok: read });++ assert.equal(supply.status, 200, supply.text);++ assert.equal(supply.body.dry_run, true);++ assert.equal(supply.body.market.read_at, gpus.body.market.read_at);++ assert.equal(supply.body.token_costs.length, 22);++});++++t.test('close', async () => {++ if (h) await h.stop();++ if (db) await db.close();++ process.stdout.write('gpu-market: saved pages, stale cache, implied token costs and both admin sections passed\n');++});++++t.run();
@@ -88,6 +88,45 @@ state, the measured cold start (from the launch to the first answered request),
@@ -88,6 +88,45 @@ state, the measured cold start (from the launch to the first answered request),
with its price (the hour and the second), endpoint, cold start and money. "Start" and "Stop" write a request row;
with its price (the hour and the second), endpoint, cold start and money. "Start" and "Stop" write a request row;
the reconciler (one implementation, the same one the service runs) does the work.
the reconciler (one implementation, the same one the service runs) does the work.
+## Public marketplace prices, and what a token costs
+
+The console's GPUs and supply sections also show what the cards cost on the public marketplaces, read from the
+providers' own pages by `server/gpu/market.js` (migration `0061_gpu_market_prices.sql`):
+
+| Source | Public page | What it publishes |
+| --- | --- | --- |
+| Vast.ai | `POST https://console.vast.ai/api/v0/bundles/` | the offer search (on demand and the minimum interruptible bid), with region and reliability |
+| RunPod | `https://www.runpod.io/pricing` | the Secure and Community Cloud on-demand columns (neither is a spot quote) |
+| Salad | `https://salad.com/pricing/` | High (never preempted) and Lowest (most interruptible) priority tiers |
+| TensorDock | `https://www.tensordock.com/` | the public "from" prices for its most popular enterprise cards |
+| CloudRift | `https://www.cloudrift.ai/pricing` | on-demand rates; H100 and H200 are quoted on request |
+
+The cards are RTX 3090, RTX 4090, RTX 5090, A100 80GB, H100 and H200. For each one the report gives the lowest
+on-demand and the lowest interruptible/spot hour, the provider, the region when the provider states it, the
+reliability words or score it publishes, and when it was read. The job signs in nowhere, rents nothing and stops
+nothing. It refreshes when due (six hours by default) and is off in tests and under a restore drill. A source that
+fails keeps its last rows and is marked `stale`; the console can still show the last known price, labelled.
+
+Both readings and the per-token cost they imply are on the admin API:
+
+- `GET /api/v1/admin/console/gpus`: `state` (the provisioner) plus `market` and `token_costs`;
+- `GET /api/v1/admin/console/supply`: the supply report plus the same `market` and `token_costs`;
+- `POST /api/v1/admin/console/gpus/prices/refresh`: read the public pages now (operator capability; still no rent).
+
+`token_costs` is one row per catalog model, GPU it fits, price kind and provider reading:
+
+```
+USD per million output tokens = hourly USD × 1,000,000 / throughput tokens per hour
+```
+
+The throughput and its source are on the row (the catalog's planning estimate today;
+`server/gpu/catalog.js`). `is_cheapest` marks the fresh floor for that model, card and kind. Each row also carries
+`cheapest_api`: the cheapest published API price for the same model, the same model family and weights
+(`same_family_weights`), the same family, or the same uncensored weights. If no comparable API model has a price,
+it is `null` rather than a guess. The console only renders this data; the page design stays in Exchange.
+
+Test coverage is `test/gpu-market.test.js` with saved pages in `test/fixtures/gpu-market/`; it makes no live call.
+
## Your own endpoint (the private rent)
## Your own endpoint (the private rent)
The Exchange's product line 6, "your own endpoint", is the same provisioner with a private rent on top
The Exchange's product line 6, "your own endpoint", is the same provisioner with a private rent on top
@@ -38,6 +38,12 @@ records: `provider` (vast/runpod), `gpu`, `region`, `hourly_usd`, `vram_mb`, `so
@@ -38,6 +38,12 @@ records: `provider` (vast/runpod), `gpu`, `region`, `hourly_usd`, `vram_mb`, `so
It requires the same operator capability as provider configuration. The parser reads bytes only. Stale quotes (over seven
It requires the same operator capability as provider configuration. The parser reads bytes only. Stale quotes (over seven
days) and missing throughput never cause a rent proposal. Recording listings does not rent anything or contact providers.
days) and missing throughput never cause a rent proposal. Recording listings does not rent anything or contact providers.
+The same two console reads also carry the public marketplace readings and the per-token cost they imply:
+`market` (Vast.ai, RunPod, Salad, TensorDock and CloudRift, cached in `gpu_market_prices`) and `token_costs`
+(catalog throughput × hourly price, beside `cheapest_api`). The reader is `server/gpu/market.js`; it reads public
+pages only, never signs in, never rents, and a failed source keeps its last rows marked `stale`. `POST
+/api/v1/admin/console/gpus/prices/refresh` forces a read. See [ai-gpus.md](ai-gpus.md#public-marketplace-prices-and-what-a-token-costs).
+
Migration 0052 is expand-only. These changes need money/security review before deployment; this PR does not deploy.
Migration 0052 is expand-only. These changes need money/security review before deployment; this PR does not deploy.
## Public market observations
## Public market observations
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+-- phase: expand
+-- The public GPU marketplace prices the owner compares against API tokens. One row is the lowest price a
+-- provider publishes right now for one card and one kind; a source that fails keeps its rows, and
+-- gpu_market_sources records that the last read failed so the console can mark them stale.
+CREATE TABLE IF NOT EXISTS gpu_market_prices (
+ provider text NOT NULL,
+ gpu_key text NOT NULL,
+ gpu text NOT NULL,
+ kind text NOT NULL CHECK(kind IN ('on_demand', 'spot')),
+ hourly_usd double precision NOT NULL CHECK(hourly_usd > 0),
+ region text NOT NULL DEFAULT '',
+ reliability double precision,
+ note text NOT NULL DEFAULT '',
+ variant text NOT NULL DEFAULT '',
+ source text NOT NULL,
+ observed_at text NOT NULL,
+ PRIMARY KEY(provider, gpu_key, kind)
+);
+CREATE TABLE IF NOT EXISTS gpu_market_sources (
+ provider text PRIMARY KEY,
+ label text NOT NULL,
+ source text NOT NULL,
+ status text NOT NULL CHECK(status IN ('ok', 'failed')),
+ read_at text,
+ ok_at text,
+ error text,
+ offers integer NOT NULL DEFAULT 0
+);
@@ -7,11 +7,20 @@ const { AiError } = require('../util');
@@ -7,11 +7,20 @@ const { AiError } = require('../util');
const providerPresets = require('../console/presets');
const providerPresets = require('../console/presets');
const providerCatalog = require('../providers/catalog');
const providerCatalog = require('../providers/catalog');
const poolSeed = require('../providers/pool-seed');
const poolSeed = require('../providers/pool-seed');
+const gpuCatalog = require('../gpu/catalog');
+const { tokenCosts } = require('../gpu/token-cost');
/** Read-only JSON read models for Exchange's owner console. Every query is bounded and the query module redacts runs. */
/** Read-only JSON read models for Exchange's owner console. Every query is bounded and the query module redacts runs. */
-function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, gpus = null, abuse = null, priceBook = null, creditGrants = null, sessionCookies = null, vault = null, settings = null, config = {}, log = console }) {
+function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, gpus = null, gpuMarket = null, abuse = null, priceBook = null, creditGrants = null, sessionCookies = null, vault = null, settings = null, config = {}, log = console }) {
const r = express.Router();
const r = express.Router();
const supply = require('../platform/supply').createSupply({ db, registry, clock });
const supply = require('../platform/supply').createSupply({ db, registry, clock });
+ // The public marketplace readings and the per-token cost they imply, added to both console sections. Data only:
+ // Exchange renders it with its existing console classes.
+ const marketPayload = async () => {
+ if (!gpuMarket) return { market: null, token_costs: [] };
+ const market = await gpuMarket.report();
+ return { market, token_costs: tokenCosts({ market, models: await registry.listModels(), catalog: gpuCatalog }) };
+ };
const json = express.json({ limit: '64kb' });
const json = express.json({ limit: '64kb' });
const read = auth.requireCap(CAPS.usageRead);
const read = auth.requireCap(CAPS.usageRead);
const write = auth.requireCap(CAPS.providerManage);
const write = auth.requireCap(CAPS.providerManage);
@@ -26,7 +35,7 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,
@@ -26,7 +35,7 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,
};
};
r.get('/api/v1/admin/console/:section', read, wrap(async (req) => {
r.get('/api/v1/admin/console/:section', read, wrap(async (req) => {
const section = req.params.section;
const section = req.params.section;
- if (section === 'supply') return supply.report();
+ if (section === 'supply') return { ...await supply.report(), ...await marketPayload() };
if (section === 'overview') {
if (section === 'overview') {
const since = new Date(clock.now() - 86400000).toISOString();
const since = new Date(clock.now() - 86400000).toISOString();
return { status: await ops.status(), counts24: await q.statusCounts(db, since), failures24: await q.failures(db, since), queue: runs.stats(), cache: await cache.stats() };
return { status: await ops.status(), counts24: await q.statusCounts(db, since), failures24: await q.failures(db, since), queue: runs.stats(), cache: await cache.stats() };
@@ -70,7 +79,7 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,
@@ -70,7 +79,7 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,
const result = await q.listAudit(db, filters);
const result = await q.listAudit(db, filters);
return { rows: result.rows, next: result.next, filters: form, invalid_filters: bad, kinds: q.AUDIT_KINDS };
return { rows: result.rows, next: result.next, filters: form, invalid_filters: bad, kinds: q.AUDIT_KINDS };
}
}
- if (section === 'gpus') return { state: gpus ? await gpus.state() : null };
+ if (section === 'gpus') return { state: gpus ? await gpus.state() : null, ...await marketPayload() };
if (section === 'pricebook') return { report: priceBook ? priceBook.last : null, state: priceBook ? await priceBook.state.read() : null,
if (section === 'pricebook') return { report: priceBook ? priceBook.last : null, state: priceBook ? await priceBook.state.read() : null,
pending: priceBook ? await priceBook.pending(50) : [], recent: priceBook ? await priceBook.changes({ status: 'applied', limit: 50 }) : [] };
pending: priceBook ? await priceBook.pending(50) : [], recent: priceBook ? await priceBook.changes({ status: 'applied', limit: 50 }) : [] };
if (section === 'exchange') return { credits: await q.settleQueue(db, clock.now()), routing: await q.routingEvidence(db) };
if (section === 'exchange') return { credits: await q.settleQueue(db, clock.now()), routing: await q.routingEvidence(db) };
@@ -82,6 +91,11 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,
@@ -82,6 +91,11 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,
if (!gpus) throw new AiError(409, 'ai.conflict', 'GPU quotes are off on this host');
if (!gpus) throw new AiError(409, 'ai.conflict', 'GPU quotes are off on this host');
return await gpus.preview(String(req.params.model), { strategy: req.query.strategy || undefined });
return await gpus.preview(String(req.params.model), { strategy: req.query.strategy || undefined });
}));
}));
+ // Read the public marketplace pages now instead of waiting for the due tick. It still never rents anything.
+ r.post('/api/v1/admin/console/gpus/prices/refresh', write, wrap(async () => {
+ if (!gpuMarket) throw new AiError(409, 'ai.conflict', 'GPU price sources are off on this host');
+ return { ...await gpuMarket.refresh(), ...await marketPayload() };
+ }));
r.post('/api/v1/admin/console/supply/quotes', json, write, wrap(async req => ({ recorded: await require('../pricebook/gpus').recordGpuQuotes(db, JSON.stringify(req.body.quotes)), dry_run: true })));
r.post('/api/v1/admin/console/supply/quotes', json, write, wrap(async req => ({ recorded: await require('../pricebook/gpus').recordGpuQuotes(db, JSON.stringify(req.body.quotes)), dry_run: true })));
r.post('/api/v1/admin/console/supply', json, write, wrap(req => supply.save(req.body || {}, actor(req))));
r.post('/api/v1/admin/console/supply', json, write, wrap(req => supply.save(req.body || {}, actor(req))));
r.post('/api/v1/admin/console/providers/:key/key', json, write, wrap(async (req) => {
r.post('/api/v1/admin/console/providers/:key/key', json, write, wrap(async (req) => {
@@ -22,7 +22,7 @@ const pkg = require('../package.json');
@@ -22,7 +22,7 @@ const pkg = require('../package.json');
const { adminRoutes } = require('openvibe-shared/config');
const { adminRoutes } = require('openvibe-shared/config');
const ownerInputs = require('./owner-inputs');
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, 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 }) {
const app = express();
const app = express();
// /.well-known/agent.json (openvibe-shared/agent-layer, SEO-2): what this site serves, for agents.
// /.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') });
require('openvibe-shared/agent-layer').mount(app, { descriptor: require('./agent-descriptor') });
@@ -232,7 +232,7 @@ function createApp({ choices = null, searchFetch = null, config, db, registry, p
@@ -232,7 +232,7 @@ function createApp({ choices = null, searchFetch = null, config, db, registry, p
// dashboard, an API for Exchange. Nothing here writes an offer; the offer belongs to Network and the Link owner.
// dashboard, an API for Exchange. Nothing here writes an offer; the offer belongs to Network and the Link owner.
if (community) app.use('/api/v1', require('./api/community').communityRouter({ community, auth, log }));
if (community) app.use('/api/v1', require('./api/community').communityRouter({ community, auth, log }));
app.use(adminRouter({ config, db, registry, pool, quotas, cache, runs, auth, settings, ops, log }));
app.use(adminRouter({ config, db, registry, pool, quotas, cache, runs, auth, settings, ops, log }));
- app.use(require('./api/console-data').consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth, clock, fetchImpl, gpus, abuse, priceBook, creditGrants, sessionCookies, vault, settings, config, log }));
+ app.use(require('./api/console-data').consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth, clock, fetchImpl, gpus, gpuMarket, abuse, priceBook, creditGrants, sessionCookies, vault, settings, config, log }));
// The operator console moved to OpenVibe.Exchange. Keep the legacy AI paths as permanent redirects;
// The operator console moved to OpenVibe.Exchange. Keep the legacy AI paths as permanent redirects;
// run receipts still use the person-session routes below and the AI JSON admin APIs stay here.
// run receipts still use the person-session routes below and the AI JSON admin APIs stay here.
// Person sessions are separate, read-only and open one run receipt each (server/person-session.js).
// Person sessions are separate, read-only and open one run receipt each (server/person-session.js).
@@ -418,6 +418,21 @@ function load(env = process.env) {
@@ -418,6 +418,21 @@ function load(env = process.env) {
thresholdPct: Number(env.AI_PRICE_BOOK_THRESHOLD_PCT || 20) / 100,
thresholdPct: Number(env.AI_PRICE_BOOK_THRESHOLD_PCT || 20) / 100,
},
},
+ // ── Public GPU marketplace prices (server/gpu/market.js) ──
+ // The small job that reads Vast.ai, RunPod, Salad, TensorDock and CloudRift's public pages for the cards the
+ // provisioner uses (and the next sizes up). It never signs in, rents or stops anything. The readings are
+ // cached in gpu_market_prices; a failed source keeps its last rows, marked stale. Off under a drill and in
+ // test mode, where tests replay saved pages.
+ gpuMarket: {
+ enabled: bool(env.AI_GPU_MARKET, !bool(env.AI_DRILL, false) && nodeEnv !== 'test'),
+ // How long a reading stays fresh, and how often the job looks whether a refresh is due.
+ intervalMs: Math.max(3600000, int(env.AI_GPU_MARKET_INTERVAL_MS, 6 * 60 * 60 * 1000)),
+ tickMs: Math.max(60000, int(env.AI_GPU_MARKET_TICK_MS, 60 * 60 * 1000)),
+ firstTickMs: Math.max(1000, int(env.AI_GPU_MARKET_FIRST_TICK_MS, 2 * 60 * 1000)),
+ staleMs: Math.max(3600000, int(env.AI_GPU_MARKET_STALE_MS, 12 * 60 * 60 * 1000)),
+ timeoutMs: Math.max(1000, int(env.AI_GPU_MARKET_TIMEOUT_MS, 20000)),
+ },
+
// ── Hourly provider balance probe (server/providers/balance.js) ──
// ── Hourly provider balance probe (server/providers/balance.js) ──
// Each active provider with a balance API (DeepSeek today) is asked GET <origin>/user/balance and the reading
// Each active provider with a balance API (DeepSeek today) is asked GET <origin>/user/balance and the reading
// is exposed as ai_provider_balance_usd{provider} on GET /metrics. Off under a restore drill (AI_DRILL=1).
// is exposed as ai_provider_balance_usd{provider} on GET /metrics. Off under a restore drill (AI_DRILL=1).
@@ -23,6 +23,8 @@ const MODELS = Object.freeze([
@@ -23,6 +23,8 @@ const MODELS = Object.freeze([
provider_key: 'gpu.deepseek-r1-32b-abliterated',
provider_key: 'gpu.deepseek-r1-32b-abliterated',
model_key: 'deepseek-r1-32b-abliterated',
model_key: 'deepseek-r1-32b-abliterated',
marketplace: 'vast',
marketplace: 'vast',
+ family: 'deepseek',
+ uncensored: true,
context_window: 8192,
context_window: 8192,
throughput_tokens_per_hour: 180000,
throughput_tokens_per_hour: 180000,
requirements: { vram_mb: 22000, disk_gb: 60, gpu_names: ['RTX 3090', 'RTX 4090'], num_gpus: 1, min_reliability: 0.99 },
requirements: { vram_mb: 22000, disk_gb: 60, gpu_names: ['RTX 3090', 'RTX 4090'], num_gpus: 1, min_reliability: 0.99 },
@@ -46,6 +48,8 @@ const MODELS = Object.freeze([
@@ -46,6 +48,8 @@ const MODELS = Object.freeze([
provider_key: 'gpu.deepseek-r1-32b',
provider_key: 'gpu.deepseek-r1-32b',
model_key: 'deepseek-r1-32b',
model_key: 'deepseek-r1-32b',
marketplace: 'vast',
marketplace: 'vast',
+ family: 'deepseek',
+ uncensored: false,
context_window: 8192,
context_window: 8192,
throughput_tokens_per_hour: 180000,
throughput_tokens_per_hour: 180000,
requirements: { vram_mb: 22000, disk_gb: 60, gpu_names: ['RTX 3090', 'RTX 4090'], num_gpus: 1, min_reliability: 0.99 },
requirements: { vram_mb: 22000, disk_gb: 60, gpu_names: ['RTX 3090', 'RTX 4090'], num_gpus: 1, min_reliability: 0.99 },
new file mode 100644
new file mode 100644
@@ -0,0 +1,440 @@
@@ -0,0 +1,440 @@
+'use strict';
+/**
+ * Public GPU marketplace prices: what the cards the provisioner uses (and the next sizes up) cost per hour,
+ * on demand and interruptible, read from the providers' own public pages.
+ *
+ * Vast.ai POST /api/v0/bundles/ the public offer search (no account is needed to search)
+ * RunPod GET /pricing the public price table (secure and community columns)
+ * Salad GET /pricing/ the public priority tiers (high is never preempted, lowest is)
+ * TensorDock GET / the public "from" prices on its home page
+ * CloudRift GET /pricing the public on-demand table (H100/H200 are quoted on request)
+ *
+ * The rules:
+ *
+ * - Read only public pages. No account, key, rental, start or stop is ever made here.
+ * - One row per provider, card and kind: the lowest price that provider publishes right now. A source that
+ * cannot be read changes nothing; its last rows stay and are marked stale until it answers again.
+ * - The reading carries where it came from, when it was read, the region when the provider states one, and
+ * the reliability words the provider publishes. A number without those is not a price the owner can trust.
+ *
+ * The cache is in the database (migrations/0061_gpu_market_prices.sql) because AI is deployed several times a
+ * day: a timer in the process would be reset by every deploy. The refresh is due-based, like the price book.
+ */
+const { AiError } = require('../util');
+
+const HOUR_MS = 60 * 60 * 1000;
+const DEFAULT_INTERVAL_MS = 6 * HOUR_MS;
+const DEFAULT_TICK_MS = HOUR_MS;
+const DEFAULT_FIRST_TICK_MS = 2 * 60 * 1000;
+const DEFAULT_STALE_MS = 12 * HOUR_MS;
+const DEFAULT_TIMEOUT_MS = 20000;
+const MAX_BYTES = 4 * 1024 * 1024;
+
+/**
+ * The cards the owner asked about: the two the catalog runs on, and the next sizes up. `minVramMb` keeps a
+ * 40 GB A100 out of the 80 GB row (Vast lists both under the same family names).
+ */
+const TARGETS = Object.freeze([
+ { key: 'rtx-3090', gpu: 'RTX 3090', vram_mb: 24576, minVramMb: 24000, aliases: ['RTX 3090'] },
+ { key: 'rtx-4090', gpu: 'RTX 4090', vram_mb: 24576, minVramMb: 24000, aliases: ['RTX 4090'] },
+ { key: 'rtx-5090', gpu: 'RTX 5090', vram_mb: 32768, minVramMb: 32000, aliases: ['RTX 5090'] },
+ { key: 'a100-80gb', gpu: 'A100 80GB', vram_mb: 81920, minVramMb: 80000, aliases: ['A100 SXM4', 'A100 PCIE', 'A100 80GB', 'A100-SXM4', 'A100-PCIE'] },
+ { key: 'h100', gpu: 'H100', vram_mb: 81920, minVramMb: 80000, aliases: ['H100 SXM', 'H100 SXM5', 'H100 PCIE', 'H100 NVL', 'H100 80GB'] },
+ { key: 'h200', gpu: 'H200', vram_mb: 141312, minVramMb: 140000, aliases: ['H200', 'H200 NVL', 'H200 SXM'] },
+]);
+const BY_KEY = new Map(TARGETS.map((t) => [t.key, t]));
+
+const VAST_NAMES = TARGETS.flatMap((t) => t.aliases.filter((a) => !/^(A100 80GB|H100 80GB|H100 SXM5)$/.test(a)));
+
+function nameOf(value) {
+ return String(value == null ? '' : value).trim().toUpperCase().replace(/\s+/g, ' ');
+}
+
+/** The target a marketplace name (and, when it publishes one, its memory) names, or null. */
+function targetFor(name, memoryMb = null) {
+ const normalized = nameOf(name);
+ for (const target of TARGETS) {
+ if (!target.aliases.some((a) => nameOf(a) === normalized)) continue;
+ if (memoryMb != null && Number.isFinite(Number(memoryMb)) && Number(memoryMb) < target.minVramMb) continue;
+ return target;
+ }
+ return null;
+}
+
+function usd(value) {
+ if (value === null || value === undefined || value === '') return null;
+ const n = Number(value);
+ return Number.isFinite(n) && n > 0 ? n : null;
+}
+
+function reliabilityOf(value) {
+ if (value == null || value === '') return null;
+ const n = Number(value);
+ return Number.isFinite(n) && n >= 0 && n <= 1 ? n : null;
+}
+
+/** A reading in the one shape every source returns. */
+function reading(target, kind, price, { region = '', reliability = null, note = '', source, variant = '' } = {}) {
+ if (!target || !usd(price)) return null;
+ return {
+ gpu_key: target.key,
+ gpu: target.gpu,
+ vram_mb: target.vram_mb,
+ kind,
+ hourly_usd: usd(price),
+ region: String(region || ''),
+ reliability,
+ note,
+ variant: String(variant || ''),
+ source,
+ };
+}
+
+/** Vast.ai's public offer JSON: one reading per target, the caller keeps the lowest per provider/key/kind. */
+function parseVast(bytes, kind) {
+ const body = typeof bytes === 'string' ? JSON.parse(bytes) : bytes;
+ const offers = (body && body.offers) || [];
+ const out = [];
+ for (const offer of offers) {
+ // dph_total is the whole single-GPU hour (GPU plus the offer's storage); min_bid is the interruptible bid,
+ // with storage billed on top. A multi-GPU offer's total is not comparable with a one-GPU hour.
+ if (Number(offer.num_gpus) !== 1) continue;
+ const target = targetFor(offer.gpu_name, offer.gpu_ram);
+ if (!target) continue;
+ const price = kind === 'spot' ? offer.min_bid : offer.dph_total;
+ const verification = String(offer.verification || 'unknown');
+ const note = kind === 'spot'
+ ? `minimum bid; storage billed on top; ${verification} host`
+ : `total includes the offer's storage; ${verification} host`;
+ const row = reading(target, kind, price, {
+ region: offer.geolocation || '',
+ reliability: reliabilityOf(offer.reliability2 == null ? offer.reliability : offer.reliability2),
+ note,
+ variant: String(offer.gpu_name || ''),
+ source: 'https://vast.ai/pricing',
+ });
+ if (row) out.push(row);
+ }
+ return out;
+}
+
+/** RunPod publishes on-demand rates for both Secure and Community Cloud; neither column is a spot quote. */
+function parseRunpod(html) {
+ const out = [];
+ for (const chunk of String(html).split(/class="gpu_table-row"/).slice(1)) {
+ const name = (chunk.match(/class="gpu_name[^"]*">([^<]+)</) || [])[1];
+ if (!name) continue;
+ const target = targetFor(name);
+ if (!target) continue;
+ const secure = (chunk.match(/data-secure="([\d.]+)"/) || [])[1];
+ const community = (chunk.match(/data-community="([\d.]+)"/) || [])[1];
+ const on = reading(target, 'on_demand', secure, {
+ note: 'secure cloud (verified hosts)', variant: name.trim(), source: 'https://www.runpod.io/pricing',
+ });
+ const communityRate = reading(target, 'on_demand', community, {
+ note: 'community cloud on-demand (third-party hosts; no spot quote published)',
+ variant: name.trim(), source: 'https://www.runpod.io/pricing',
+ });
+ if (on) out.push(on);
+ if (communityRate) out.push(communityRate);
+ }
+ return out;
+}
+
+/** Salad's price table: High is never preempted; Lowest is the most interruptible. */
+function parseSalad(html) {
+ const match = String(html).match(/<script[^>]*id=["']?gpu-prices["']?[^>]*>([\s\S]*?)<\/script>/i);
+ if (!match) throw new Error('Salad pricing page has no gpu-prices table');
+ const rows = JSON.parse(match[1]);
+ const out = [];
+ for (const row of Array.isArray(rows) ? rows : []) {
+ const target = targetFor(row && row.name);
+ if (!target) continue;
+ const on = reading(target, 'on_demand', row.high, {
+ note: 'high priority tier (never preempted by other workloads); vCPU and RAM included',
+ variant: String(row.name || ''), source: 'https://salad.com/pricing/',
+ });
+ const spot = reading(target, 'spot', row.lowest, {
+ note: 'lowest priority tier (the most interruptible); vCPU and RAM included',
+ variant: String(row.name || ''), source: 'https://salad.com/pricing/',
+ });
+ if (on) out.push(on);
+ if (spot) out.push(spot);
+ }
+ return out;
+}
+
+/** TensorDock's home page publishes a "from" price for its two most popular enterprise cards. */
+function parseTensordock(html) {
+ const text = String(html).replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')
+ .replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/\s+/g, ' ');
+ const out = [];
+ for (const [key, re] of [
+ ['h100', /H100 SXM5\s+From\s+\$([\d.]+)\s*\/hr/i],
+ ['a100-80gb', /A100 SXM4\s+From\s+\$([\d.]+)\s*\/hr/i],
+ ]) {
+ const m = text.match(re);
+ if (!m) continue;
+ const target = BY_KEY.get(key);
+ const row = reading(target, 'on_demand', m[1], {
+ note: 'published "from" price on the TensorDock home page; the full marketplace list is behind its dashboard; no reliability score published',
+ variant: key === 'h100' ? 'H100 SXM5' : 'A100 SXM4',
+ source: 'https://www.tensordock.com/',
+ });
+ if (row) out.push(row);
+ }
+ return out;
+}
+
+/** CloudRift's public table: only the cards whose on-demand rate is a number; its H100/H200 say "Upon request". */
+function parseCloudrift(html) {
+ const text = String(html).replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')
+ .replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/\s+/g, ' ');
+ const out = [];
+ for (const [name, key] of [['RTX 4090', 'rtx-4090'], ['RTX 5090', 'rtx-5090'], ['A100 SXM4', 'a100-80gb'], ['H100', 'h100'], ['H200', 'h200']]) {
+ const normalized = name.replace(/ /g, '\\s+');
+ const m = text.match(new RegExp(normalized + '\\s+NVIDIA\\s+On.?Demand\\s+\\$([\\d.]+)', 'i'));
+ if (!m) continue;
+ const row = reading(BY_KEY.get(key), 'on_demand', m[1], {
+ note: 'on-demand rate from the public table; reserved rates are lower, H100/H200 on demand are quoted on request, and no reliability score is published',
+ variant: name, source: 'https://www.cloudrift.ai/pricing',
+ });
+ if (row) out.push(row);
+ }
+ return out;
+}
+
+async function read(fetchImpl, url, { method = 'GET', body = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
+ const headers = { Accept: 'application/json, text/html;q=0.9', 'User-Agent': 'OpenVibe.AI GPU price reader (+https://openvibe.exchange)' };
+ if (body != null) headers['Content-Type'] = 'application/json';
+ let res;
+ try {
+ res = await fetchImpl(url, {
+ method, headers, redirect: 'follow', signal: AbortSignal.timeout(Math.max(1000, timeoutMs)),
+ body: body != null ? JSON.stringify(body) : undefined,
+ });
+ } catch (err) {
+ throw new Error(`${url} did not answer: ${String(err && err.message || err).slice(0, 120)}`);
+ }
+ if (!res || !res.ok) throw new Error(`${url} answered ${res ? res.status : 'no status'}`);
+ const text = await res.text();
+ if (text.length > MAX_BYTES) throw new Error(`${url} answered ${text.length} bytes (over ${MAX_BYTES})`);
+ return text;
+}
+
+const SOURCES = Object.freeze([
+ {
+ id: 'vast', label: 'Vast.ai', source: 'https://vast.ai/pricing',
+ async read({ fetchImpl, timeoutMs }) {
+ const body = (type, order) => ({
+ type, gpu_name: { in: VAST_NAMES }, num_gpus: { eq: 1 }, rentable: { eq: true }, limit: 500,
+ order: [[order, 'asc']],
+ });
+ const [onDemand, bid] = await Promise.all([
+ read(fetchImpl, 'https://console.vast.ai/api/v0/bundles/', { method: 'POST', body: body('ondemand', 'dph_total'), timeoutMs }),
+ read(fetchImpl, 'https://console.vast.ai/api/v0/bundles/', { method: 'POST', body: body('bid', 'min_bid'), timeoutMs }),
+ ]);
+ return [...parseVast(onDemand, 'on_demand'), ...parseVast(bid, 'spot')];
+ },
+ },
+ {
+ id: 'runpod', label: 'RunPod', source: 'https://www.runpod.io/pricing',
+ async read({ fetchImpl, timeoutMs }) {
+ return parseRunpod(await read(fetchImpl, 'https://www.runpod.io/pricing', { timeoutMs }));
+ },
+ },
+ {
+ id: 'salad', label: 'Salad', source: 'https://salad.com/pricing/',
+ async read({ fetchImpl, timeoutMs }) {
+ return parseSalad(await read(fetchImpl, 'https://salad.com/pricing/', { timeoutMs }));
+ },
+ },
+ {
+ id: 'tensordock', label: 'TensorDock', source: 'https://www.tensordock.com/',
+ async read({ fetchImpl, timeoutMs }) {
+ return parseTensordock(await read(fetchImpl, 'https://www.tensordock.com/', { timeoutMs }));
+ },
+ },
+ {
+ id: 'cloudrift', label: 'CloudRift', source: 'https://www.cloudrift.ai/pricing',
+ async read({ fetchImpl, timeoutMs }) {
+ return parseCloudrift(await read(fetchImpl, 'https://www.cloudrift.ai/pricing', { timeoutMs }));
+ },
+ },
+]);
+const SOURCE_BY_ID = new Map(SOURCES.map((s) => [s.id, s]));
+
+/** The lowest reading per (provider, card, kind): one row is what a provider actually charges for that hour. */
+function lowest(readings) {
+ const out = new Map();
+ for (const row of readings || []) {
+ if (!row || !usd(row.hourly_usd)) continue;
+ const key = `${row.gpu_key}\u0000${row.kind}`;
+ const held = out.get(key);
+ if (!held || row.hourly_usd < held.hourly_usd) out.set(key, row);
+ }
+ return [...out.values()];
+}
+
+function createGpuMarket({ db, config = {}, clock = { now: () => Date.now() }, fetchImpl = null, log = console } = {}) {
+ const settings = {
+ enabled: false,
+ intervalMs: DEFAULT_INTERVAL_MS,
+ tickMs: DEFAULT_TICK_MS,
+ firstTickMs: DEFAULT_FIRST_TICK_MS,
+ staleMs: DEFAULT_STALE_MS,
+ timeoutMs: DEFAULT_TIMEOUT_MS,
+ ...(config.gpuMarket || {}),
+ };
+ const iso = (at = clock.now()) => new Date(at).toISOString();
+ let timer = null;
+ let first = null;
+ let busy = false;
+
+ /** Fetch every source and write what it said; a failed source leaves its last readings in place. */
+ async function refresh({ fetchImpl: seam = null } = {}) {
+ const fetcher = seam || fetchImpl;
+ if (!fetcher) throw new AiError(503, 'gpu.market_unavailable', 'No GPU price transport is available');
+ const outcomes = await Promise.all(SOURCES.map(async (source) => {
+ try {
+ const rows = await source.read({ fetchImpl: fetcher, timeoutMs: settings.timeoutMs });
+ return { source, rows: lowest(rows) };
+ } catch (err) {
+ return { source, error: String(err && err.message || err).slice(0, 300) };
+ }
+ }));
+ const at = iso();
+ let refreshed = 0;
+ let failed = 0;
+ for (const outcome of outcomes) {
+ const { source } = outcome;
+ if (outcome.error) {
+ failed += 1;
+ await db.prepare(`INSERT INTO gpu_market_sources(provider,label,source,status,read_at,ok_at,error,offers)
+ VALUES(?,?,?,?,?,NULL,?,0)
+ ON CONFLICT(provider) DO UPDATE SET status=excluded.status, read_at=excluded.read_at,
+ error=excluded.error, label=excluded.label, source=excluded.source`)
+ .run(source.id, source.label, source.source, 'failed', at, outcome.error);
+ continue;
+ }
+ refreshed += 1;
+ await db.prepare('DELETE FROM gpu_market_prices WHERE provider = ?').run(source.id);
+ for (const row of outcome.rows) {
+ await db.prepare(`INSERT INTO gpu_market_prices(provider,gpu_key,gpu,kind,hourly_usd,region,reliability,note,variant,source,observed_at)
+ VALUES(?,?,?,?,?,?,?,?,?,?,?)
+ ON CONFLICT(provider,gpu_key,kind) DO UPDATE SET hourly_usd=excluded.hourly_usd, region=excluded.region,
+ reliability=excluded.reliability, note=excluded.note, variant=excluded.variant, source=excluded.source,
+ observed_at=excluded.observed_at`)
+ .run(source.id, row.gpu_key, row.gpu, row.kind, row.hourly_usd, row.region, row.reliability,
+ row.note, row.variant, row.source, at);
+ }
+ await db.prepare(`INSERT INTO gpu_market_sources(provider,label,source,status,read_at,ok_at,error,offers)
+ VALUES(?,?,?,?,?,?,NULL,?)
+ ON CONFLICT(provider) DO UPDATE SET status='ok', read_at=excluded.read_at, ok_at=excluded.ok_at,
+ error=NULL, offers=excluded.offers, label=excluded.label, source=excluded.source`)
+ .run(source.id, source.label, source.source, 'ok', at, at, outcome.rows.length);
+ }
+ return { refreshed, failed, sources: (await report()).sources };
+ }
+
+ /** The cached readings, with the stale facts: a failed source keeps its last numbers, marked stale. */
+ async function report() {
+ const [sourceRows, priceRows] = await Promise.all([
+ db.prepare('SELECT * FROM gpu_market_sources').all().catch(() => []),
+ db.prepare('SELECT * FROM gpu_market_prices').all().catch(() => []),
+ ]);
+ const now = clock.now();
+ const state = new Map((sourceRows || []).map((row) => [row.provider, row]));
+ const sources = SOURCES.map((source) => {
+ const row = state.get(source.id) || null;
+ const okAt = row && row.ok_at ? Date.parse(row.ok_at) : NaN;
+ const stale = !row || row.status !== 'ok' || !Number.isFinite(okAt) || now - okAt > settings.staleMs;
+ return {
+ provider: source.id,
+ label: source.label,
+ source: source.source,
+ status: row ? String(row.status) : 'never',
+ read_at: row && row.read_at ? String(row.read_at) : null,
+ ok_at: row && row.ok_at ? String(row.ok_at) : null,
+ stale,
+ error: row && row.error ? String(row.error) : null,
+ offers: row ? Number(row.offers) || 0 : 0,
+ };
+ });
+ const sourceState = new Map(sources.map((s) => [s.provider, s]));
+ const rows = (priceRows || []).map((row) => ({ ...row, hourly_usd: Number(row.hourly_usd), reliability: row.reliability == null ? null : Number(row.reliability) }))
+ .filter((row) => usd(row.hourly_usd));
+ const byTarget = new Map(TARGETS.map((t) => [t.key, []]));
+ for (const row of rows) {
+ if (!byTarget.has(row.gpu_key)) continue;
+ const source = sourceState.get(row.provider);
+ byTarget.get(row.gpu_key).push({
+ provider: row.provider,
+ provider_label: source ? source.label : row.provider,
+ kind: row.kind,
+ hourly_usd: row.hourly_usd,
+ region: row.region || '',
+ reliability: row.reliability,
+ note: row.note || '',
+ variant: row.variant || '',
+ source: row.source,
+ observed_at: row.observed_at,
+ stale: source ? source.stale : true,
+ });
+ }
+ const best = (readings, kind) => {
+ const of = readings.filter((r) => r.kind === kind).sort((a, b) => a.hourly_usd - b.hourly_usd);
+ const fresh = of.filter((r) => !r.stale);
+ return (fresh.length ? fresh : of)[0] || null;
+ };
+ const gpus = TARGETS.map((target) => {
+ const readings = byTarget.get(target.key);
+ return {
+ key: target.key, gpu: target.gpu, vram_mb: target.vram_mb,
+ on_demand: best(readings, 'on_demand'),
+ spot: best(readings, 'spot'),
+ readings,
+ };
+ });
+ const readAt = sources.map((s) => s.ok_at).filter(Boolean).sort().pop() || null;
+ return { read_at: readAt, stale_after_ms: settings.staleMs, sources, gpus };
+ }
+
+ /** The book is due when the last answered read is older than the interval (a deploy does not reset it). */
+ async function due() {
+ const row = await db.prepare('SELECT MAX(ok_at) AS at FROM gpu_market_sources').get().catch(() => null);
+ const at = row && row.at ? Date.parse(row.at) : NaN;
+ return !Number.isFinite(at) || clock.now() - at >= settings.intervalMs;
+ }
+
+ function start() {
+ if (!settings.enabled || timer) return;
+ const tick = async () => {
+ if (busy) return;
+ busy = true;
+ try {
+ if (await due()) await refresh();
+ } catch (err) {
+ log.warn(`[ai] GPU market prices failed: ${String(err && err.message || err).slice(0, 200)}`);
+ } finally { busy = false; }
+ };
+ timer = setInterval(tick, Math.max(60000, settings.tickMs));
+ if (timer.unref) timer.unref();
+ first = setTimeout(tick, Math.max(1000, settings.firstTickMs));
+ if (first.unref) first.unref();
+ }
+
+ function stop() {
+ if (timer) clearInterval(timer);
+ if (first) clearTimeout(first);
+ timer = null;
+ first = null;
+ }
+
+ return { refresh, report, due, start, stop, settings, SOURCES, TARGETS };
+}
+
+module.exports = {
+ createGpuMarket, parseVast, parseRunpod, parseSalad, parseTensordock, parseCloudrift, targetFor, lowest,
+ TARGETS, SOURCES, SOURCE_BY_ID, HOUR_MS,
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
+'use strict';
+/**
+ * What a catalogued model costs per token on a rented GPU.
+ *
+ * hourly price from the public marketplace readings (server/gpu/market.js)
+ * × 1,000,000 / the model's tokens per hour (the catalog's planning estimate)
+ * = USD per million output tokens
+ *
+ * Beside it is the cheapest published API price for the same or a comparable model, so the owner can see
+ * whether the rent is cheaper than buying the tokens. The comparison is deliberately narrow: the exact model
+ * first, then the same family (with the same uncensored/standard weights when both say), then any priced model
+ * with the same weights. If nothing comparable is priced, there is no comparison rather than a misleading one.
+ */
+const { list: catalogModels } = require('./catalog');
+
+const THROUGHPUT_SOURCE = 'catalog planning estimate (server/gpu/catalog.js)';
+
+function number(value) {
+ if (value === null || value === undefined || value === '') return null;
+ const n = Number(value);
+ return Number.isFinite(n) ? n : null;
+}
+
+function round(value, digits = 6) {
+ const factor = 10 ** digits;
+ return Math.round(value * factor) / factor;
+}
+
+function nameOf(value) {
+ return String(value == null ? '' : value).trim().toUpperCase();
+}
+
+/**
+ * The cheapest priced API model comparable with one catalog model.
+ * → `{ ...model, match }` or null.
+ */
+function cheapestComparable(spec, models = []) {
+ const sameKeys = new Set([spec.key, spec.model_key, ...(Array.isArray(spec.api_comparables) ? spec.api_comparables : [])].filter(Boolean));
+ const family = spec.family ? String(spec.family).toLowerCase() : null;
+ const uncensored = spec.uncensored === true || spec.uncensored === false ? Boolean(spec.uncensored) : null;
+ const ranked = [];
+ for (const model of models) {
+ if (!model || String(model.provider_key || '').startsWith('gpu.')) continue;
+ const out = number(model.cost && model.cost.out_per_mtok);
+ if (out === null || out < 0) continue;
+ const modelFamily = model.family ? String(model.family).toLowerCase() : null;
+ const modelUncensored = model.uncensored === true || model.uncensored === false ? Boolean(model.uncensored) : null;
+ let match = null;
+ let score = 4;
+ if (sameKeys.has(model.model_key)) { match = 'same_model'; score = 0; }
+ else if (family && modelFamily === family) {
+ if (uncensored !== null && modelUncensored === uncensored) { match = 'same_family_weights'; score = 1; }
+ else { match = 'same_family'; score = 2; }
+ }
+ else if (uncensored === true && modelUncensored === true) { match = 'uncensored'; score = 3; }
+ if (score === 4) continue;
+ ranked.push({ ...model, match, score, out_per_mtok: out, in_per_mtok: number(model.cost && model.cost.in_per_mtok) });
+ }
+ ranked.sort((a, b) => a.score - b.score || a.out_per_mtok - b.out_per_mtok || String(a.model_key).localeCompare(String(b.model_key)));
+ const best = ranked[0];
+ if (!best) return null;
+ return {
+ model_key: best.model_key,
+ display_name: best.display_name || best.model_key,
+ provider_key: best.provider_key,
+ family: best.family || null,
+ uncensored: best.uncensored === true || best.uncensored === false ? Boolean(best.uncensored) : null,
+ in_per_mtok: best.in_per_mtok,
+ out_per_mtok: best.out_per_mtok,
+ usd_per_million_output_tokens: best.out_per_mtok,
+ usd_per_million_input_tokens: best.in_per_mtok,
+ match: best.match,
+ source: (best.cost && best.cost.source) || best.source || null,
+ };
+}
+
+/**
+ * One row per catalog model, GPU it fits, kind of price (on demand and spot) and provider reading, so the
+ * owner sees the Vast 4090 hour beside the Salad 4090 hour. `is_cheapest` marks the fresh floor for that
+ * card and kind. `market` is the report from server/gpu/market.js.
+ */
+function tokenCosts({ market = null, models = [], catalog = null } = {}) {
+ if (!market || !Array.isArray(market.gpus)) return [];
+ const catalogList = Array.isArray(catalog) ? catalog : (catalog && typeof catalog.list === 'function' ? catalog.list() : catalogModels());
+ const gpus = new Map(market.gpus.map((gpu) => [nameOf(gpu.gpu), gpu]));
+ const out = [];
+ for (const spec of catalogList) {
+ const fits = ((spec.requirements && spec.requirements.gpu_names) || []).map(nameOf).filter((name) => gpus.has(name));
+ const throughput = number(spec.throughput_tokens_per_hour);
+ if (!Number.isFinite(throughput) || throughput <= 0) continue;
+ for (const gpuName of fits) {
+ const gpu = gpus.get(gpuName);
+ const readings = Array.isArray(gpu.readings) ? gpu.readings : [];
+ for (const kind of ['on_demand', 'spot']) {
+ const best = gpu[kind];
+ for (const reading of readings.filter((r) => r.kind === kind)) {
+ const hourly = number(reading.hourly_usd);
+ if (!Number.isFinite(hourly) || hourly <= 0) continue;
+ out.push({
+ model_key: spec.key,
+ model_display_name: spec.display_name || spec.key,
+ gpu: gpu.gpu,
+ gpu_key: gpu.key,
+ kind,
+ provider: reading.provider,
+ provider_label: reading.provider_label || reading.provider,
+ region: reading.region || '',
+ hourly_usd: hourly,
+ reliability: reading.reliability == null ? null : number(reading.reliability),
+ observed_at: reading.observed_at,
+ stale: Boolean(reading.stale),
+ is_cheapest: Boolean(!reading.stale && best && best.provider === reading.provider && best.hourly_usd === reading.hourly_usd && best.observed_at === reading.observed_at),
+ source: reading.source,
+ throughput_tokens_per_hour: throughput,
+ throughput_source: THROUGHPUT_SOURCE,
+ usd_per_million_tokens: round((hourly * 1e6) / throughput),
+ cheapest_api: cheapestComparable(spec, models),
+ });
+ }
+ }
+ }
+ }
+ return out;
+}
+
+module.exports = { tokenCosts, cheapestComparable, THROUGHPUT_SOURCE };
@@ -57,6 +57,9 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
@@ -57,6 +57,9 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
// API key and a monthly cap in the owner settings (ai.gpus). The reconciler rents, health-checks and
// API key and a monthly cap in the owner settings (ai.gpus). The reconciler rents, health-checks and
// scales the instances to zero; it never spends past the cap.
// scales the instances to zero; it never spends past the cap.
const gpus = require('./gpu/provisioner').createGpuProvisioner({ db, registry, settings, clock, fetchImpl, env, log });
const gpus = require('./gpu/provisioner').createGpuProvisioner({ db, registry, settings, clock, fetchImpl, env, log });
+ // The public GPU marketplace prices (server/gpu/market.js): a due-based public-page reader for the console's
+ // supply view. It never signs in or rents; a failed source keeps its last reading, marked stale.
+ const gpuMarket = require('./gpu/market').createGpuMarket({ db, config, clock, fetchImpl, log });
const vault = require('./vault').createVault({ settings, clock, log });
const vault = require('./vault').createVault({ settings, clock, log });
await require('./vault').migrate({ db, config, settings, log }).catch((err) => log.warn(`[Vault] migration failed: ${err.message}`));
await require('./vault').migrate({ db, config, settings, log }).catch((err) => log.warn(`[Vault] migration failed: ${err.message}`));
await vault.load().catch((err) => log.warn(`[Vault] ${err.message}`));
await vault.load().catch((err) => log.warn(`[Vault] ${err.message}`));
@@ -205,7 +208,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
@@ -205,7 +208,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
clientSecret: env.OV_OAUTH_CLIENT_SECRET, fetch: fetchImpl,
clientSecret: env.OV_OAUTH_CLIENT_SECRET, fetch: fetchImpl,
}) : null;
}) : null;
const benchEvents = createBenchEvents({ bench, accountData, accountSend, config, log, fetchImpl, clientSecret: env.OV_OAUTH_CLIENT_SECRET });
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 });
+ 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 });
let jwks = null;
let jwks = null;
let keyLoaded = Promise.resolve(null);
let keyLoaded = Promise.resolve(null);
if (!keys.pinned && config.nodeEnv !== 'test') {
if (!keys.pinned && config.nodeEnv !== 'test') {
@@ -259,6 +262,8 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
@@ -259,6 +262,8 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
if (listen && config.nodeEnv !== 'test' && memorySharing) memorySharing.start();
if (listen && config.nodeEnv !== 'test' && memorySharing) memorySharing.start();
// The price book reads the providers' listings once a week; it never starts in test mode or under a drill.
// The price book reads the providers' listings once a week; it never starts in test mode or under a drill.
if (listen && config.priceBook && config.priceBook.enabled) priceBook.start();
if (listen && config.priceBook && config.priceBook.enabled) priceBook.start();
+ // The public GPU price reader: public pages only, due-based, off in tests and under a drill.
+ if (listen && config.gpuMarket && config.gpuMarket.enabled) gpuMarket.start();
// The refusal job asks the offers the same questions once a week; it never starts in test mode (the
// The refusal job asks the offers the same questions once a week; it never starts in test mode (the
// tests drive it by hand) and never under a restore drill.
// tests drive it by hand) and never under a restore drill.
if (listen && config.refusal && config.refusal.enabled) refusal.start();
if (listen && config.refusal && config.refusal.enabled) refusal.start();
@@ -305,6 +310,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
@@ -305,6 +310,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
memoryJobs.stop();
memoryJobs.stop();
if (memorySharing) memorySharing.stop();
if (memorySharing) memorySharing.stop();
priceBook.stop();
priceBook.stop();
+ gpuMarket.stop();
refusal.stop();
refusal.stop();
batches.stop();
batches.stop();
batcher.stop();
batcher.stop();
@@ -324,7 +330,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
@@ -324,7 +330,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
if (!givenDb) await db.close();
if (!givenDb) await db.close();
}
}
- return { config, db, registry, pool, quotas, tiers, cache, fetcher, engine, runs, keys, keyLoaded, auth, app, server, bench, benchEvents, memory, memoryJobs, memoryServices, memorySharing, priceBook, refusal, batches, batcher, credits: creditsRef, settings, vault, community, endpoints, webhooks, gpus, abuse, ownEndpoints, close };
+ return { config, db, registry, pool, quotas, tiers, cache, fetcher, engine, runs, keys, keyLoaded, auth, app, server, bench, benchEvents, memory, memoryJobs, memoryServices, memorySharing, priceBook, refusal, batches, batcher, credits: creditsRef, settings, vault, community, endpoints, webhooks, gpus, gpuMarket, abuse, ownEndpoints, close };
}
}
if (require.main === module) {
if (require.main === module) {
@@ -9,10 +9,16 @@ const { SOURCES } = require('../server/pricebook/sources');
@@ -9,10 +9,16 @@ const { SOURCES } = require('../server/pricebook/sources');
const { PROVIDERS } = require('../server/providers/pool-providers');
const { PROVIDERS } = require('../server/providers/pool-providers');
const { boot, request, suite } = require('./helpers');
const { boot, request, suite } = require('./helpers');
const dev = require('./fixtures/market/models-dev.json');
const dev = require('./fixtures/market/models-dev.json');
-const files = { deepseek:'deepseek.md', qwen:'qwen.html', kimi:'kimi.html', glm:'glm.md', minimax:'minimax.md', deepinfra:'deepinfra.json', novita:'novita.json', featherless:'featherless.json', refuseless:'refuseless.md', abliteration:'abliteration.md' };
+// Each own source saves its page under its own key; discover the extension so a new source does not break the replay.
+const fixtureFor = (key) => {
+ const dir = path.join(__dirname, 'fixtures/pricebook');
+ const name = fs.readdirSync(dir).find((entry) => entry.startsWith(key + '.'));
+ if (!name) throw new Error(`no saved pricebook page for ${key}`);
+ return name;
+};
const observations = [];
const observations = [];
for (const s of SOURCES.filter(s => s.role === 'own')) {
for (const s of SOURCES.filter(s => s.role === 'own')) {
- const raw = fs.readFileSync(path.join(__dirname, 'fixtures/pricebook', files[s.key] || `${s.key}.json`), 'utf8');
+ const raw = fs.readFileSync(path.join(__dirname, 'fixtures/pricebook', fixtureFor(s.key)), 'utf8');
for (const [model_key, price] of Object.entries(s.parse(s.kind === 'json' ? JSON.parse(raw) : raw))) observations.push({ provider_key: s.provider, model_key, price, source: s.url, checked_at: '2026-11-09' });
for (const [model_key, price] of Object.entries(s.parse(s.kind === 'json' ? JSON.parse(raw) : raw))) observations.push({ provider_key: s.provider, model_key, price, source: s.url, checked_at: '2026-11-09' });
}
}
const t = suite('exchange-market'); let h;
const t = suite('exchange-market'); let h;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+<!doctype html><html><body>
+<p>RTX 4090 NVIDIA On‑Demand $0.45 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>
+<p>RTX 5090 NVIDIA On‑Demand $0.65 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>
+<p>H100 NVIDIA On‑Demand Upon request 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>
+<p>H200 NVIDIA On‑Demand Upon request 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>
+<p>A100 SXM4 NVIDIA On‑Demand Upon request 1 mo — 24 GB VRAM 420 GB Storage Max 6 GPUs</p>
+</body></html>
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+<!doctype html><html><body>
+<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H200</p><div class="gpu_table-pricing"><div data-secure="5.29" data-community="3.59" class="gpu_table-price"><span class="active-price">5.29</span></div></div></div></div>
+<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 NVL</p><div class="gpu_table-pricing"><div data-secure="3.19" data-community="2.59" class="gpu_table-price"><span class="active-price">3.19</span></div></div></div></div>
+<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 PCIe</p><div class="gpu_table-pricing"><div data-secure="2.89" data-community="1.99" class="gpu_table-price"><span class="active-price">2.89</span></div></div></div></div>
+<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 SXM</p><div class="gpu_table-pricing"><div data-secure="3.99" data-community="2.69" class="gpu_table-price"><span class="active-price">3.99</span></div></div></div></div>
+<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">A100 PCIe</p><div class="gpu_table-pricing"><div data-secure="1.79" data-community="1.19" class="gpu_table-price"><span class="active-price">1.79</span></div></div></div></div>
+<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">A100 SXM</p><div class="gpu_table-pricing"><div data-secure="1.79" data-community="1.39" class="gpu_table-price"><span class="active-price">1.79</span></div></div></div></div>
+<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 5090</p><div class="gpu_table-pricing"><div data-secure="1.19" data-community="0.69" class="gpu_table-price"><span class="active-price">1.19</span></div></div></div></div>
+<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 3090</p><div class="gpu_table-pricing"><div data-secure="0.5" data-community="0.22" class="gpu_table-price"><span class="active-price">0.5</span></div></div></div></div>
+<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 4090</p><div class="gpu_table-pricing"><div data-secure="0.89" data-community="0.34" class="gpu_table-price"><span class="active-price">0.89</span></div></div></div></div>
+</body></html>
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+<!doctype html><html><body><script type=application/json id=gpu-prices>[{"name":"RTX 5090","vram_gb":32,"high":0.6,"medium":0.483,"low":0.367,"lowest":0.25},{"name":"RTX 4090","vram_gb":24,"high":0.44,"medium":0.347,"low":0.253,"lowest":0.16},{"name":"RTX 5080","vram_gb":16,"high":0.32,"medium":0.263,"low":0.207,"lowest":0.15},{"name":"RTX 5070 Ti","vram_gb":16,"high":0.27,"medium":0.213,"low":0.157,"lowest":0.1},{"name":"RTX 4080","vram_gb":16,"high":0.27,"medium":0.217,"low":0.163,"lowest":0.11},{"name":"RTX 4070 Ti Super","vram_gb":16,"high":0.24,"medium":0.19,"low":0.14,"lowest":0.09},{"name":"RTX 5090 Laptop","vram_gb":24,"high":0.235,"medium":0.19,"low":0.145,"lowest":0.1},{"name":"RTX 3090 Ti","vram_gb":24,"high":0.22,"medium":0.18,"low":0.14,"lowest":0.1},{"name":"RTX 3090","vram_gb":24,"high":0.2,"medium":0.163,"low":0.127,"lowest":0.09},{"name":"RTX 5070","vram_gb":12,"high":0.2,"medium":0.16,"low":0.12,"lowest":0.08},{"name":"RTX 4070 Ti","vram_gb":12,"high":0.2,"medium":0.16,"low":0.12,"lowest":0.08},{"name":"RTX 3080 Ti","vram_gb":12,"high":0.195,"medium":0.157,"low":0.118,"lowest":0.08},{"name":"RTX 4070","vram_gb":12,"high":0.165,"medium":0.133,"low":0.102,"lowest":0.07},{"name":"RTX 5060 Ti","vram_gb":16,"high":0.155,"medium":0.127,"low":0.098,"lowest":0.07},{"name":"RTX 3080","vram_gb":10,"high":0.155,"medium":0.123,"low":0.092,"lowest":0.06},{"name":"RTX 4060 Ti","vram_gb":16,"high":0.14,"medium":0.12,"low":0.1,"lowest":0.08},{"name":"RTX 5060","vram_gb":8,"high":0.115,"medium":0.098,"low":0.082,"lowest":0.065}]</script></body></html>
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+<!doctype html><html><body>
+<h2>Our most popular GPUs</h2>
+<p>H100 SXM5 From $2.25/hr For builders needing training and inference with no compromises.</p>
+<p>A100 SXM4 From $1.80/hr The best balance between price and performance for AI inference.</p>
+<p>Take advantage of our consumer GPUs from $0.12/hr for up to 5x inference value.</p>
+</body></html>
new file mode 100644
new file mode 100644
@@ -0,0 +1,754 @@
@@ -0,0 +1,754 @@
+{
+ "offers": [
+ {
+ "id": 20130142,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.08148148148148149,
+ "min_bid": 0.08,
+ "reliability": 0.9933032,
+ "reliability2": 0.9933032,
+ "geolocation": "Czechia, CZ",
+ "verification": "verified",
+ "storage_cost": 0.1333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 38849861,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.09555555555555556,
+ "min_bid": 0.09333333333333334,
+ "reliability": 0.9942635,
+ "reliability2": 0.9942635,
+ "geolocation": "Quebec, CA",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59612506,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 16376,
+ "num_gpus": 1,
+ "dph_total": 0.13629626296296296,
+ "min_bid": 0.1333333,
+ "reliability": 0.9353087,
+ "reliability2": 0.9353087,
+ "geolocation": "California, US",
+ "verification": "unverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59373283,
+ "gpu_name": "A100 PCIE",
+ "gpu_ram": 40960,
+ "num_gpus": 1,
+ "dph_total": 0.1355555222222222,
+ "min_bid": 0.1333333,
+ "reliability": 0.9681284,
+ "reliability2": 0.9681284,
+ "geolocation": "Beijing, CN",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 40436852,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.1348148148148148,
+ "min_bid": 0.13333333333333333,
+ "reliability": 0.9983809,
+ "reliability2": 0.9983809,
+ "geolocation": "Greece, GR",
+ "verification": "unverified",
+ "storage_cost": 0.1333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59874081,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.13555555555555554,
+ "min_bid": 0.13333333333333333,
+ "reliability": 0.9952501,
+ "reliability2": 0.9952501,
+ "geolocation": "Montana, US",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 30714840,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.14888892222222222,
+ "min_bid": 0.1466667,
+ "reliability": 0.9476724,
+ "reliability2": 0.9476724,
+ "geolocation": "The Netherlands, NL",
+ "verification": "verified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 57970526,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.16407407407407407,
+ "min_bid": 0.16,
+ "reliability": 0.9876611,
+ "reliability2": 0.9876611,
+ "geolocation": "Quebec, CA",
+ "verification": "deverified",
+ "storage_cost": 0.3666666666666667,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 47563804,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.17037037037037037,
+ "min_bid": 0.16,
+ "reliability": 0.9741023,
+ "reliability2": 0.9741023,
+ "geolocation": "Quebec, CA",
+ "verification": "verified",
+ "storage_cost": 0.9333333333333335,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 27177907,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.1648888888888889,
+ "min_bid": 0.16,
+ "reliability": 0.9868965,
+ "reliability2": 0.9868965,
+ "geolocation": "Czechia, CZ",
+ "verification": "deverified",
+ "storage_cost": 0.44,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58273870,
+ "gpu_name": "A100 PCIE",
+ "gpu_ram": 81920,
+ "num_gpus": 1,
+ "dph_total": 0.16822222222222222,
+ "min_bid": 0.166,
+ "reliability": 0.8620362,
+ "reliability2": 0.8620362,
+ "geolocation": "United Kingdom, GB",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 57810853,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.19555555555555554,
+ "min_bid": 0.19333333333333333,
+ "reliability": 0.965621,
+ "reliability2": 0.965621,
+ "geolocation": "Thailand, TH",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58831870,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.2022222222222222,
+ "min_bid": 0.19999999999999998,
+ "reliability": 0.9988663,
+ "reliability2": 0.9988663,
+ "geolocation": "Quebec, CA",
+ "verification": "verified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 56007282,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.20222222222222222,
+ "min_bid": 0.2,
+ "reliability": 0.9974512,
+ "reliability2": 0.9974512,
+ "geolocation": "Nevada, US",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 56141038,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.21629629629629632,
+ "min_bid": 0.21333333333333335,
+ "reliability": 0.9533946,
+ "reliability2": 0.9533946,
+ "geolocation": "Quebec, CA",
+ "verification": "deverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 9817760,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.22165925925925925,
+ "min_bid": 0.22,
+ "reliability": 0.9991736,
+ "reliability2": 0.9991736,
+ "geolocation": "Ireland, IE",
+ "verification": "deverified",
+ "storage_cost": 0.14933333333333335,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59877086,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.2277037037037037,
+ "min_bid": 0.224,
+ "reliability": 0.9731478,
+ "reliability2": 0.9731478,
+ "geolocation": "Vietnam, VN",
+ "verification": "verified",
+ "storage_cost": 0.3333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58120176,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.23296296296296298,
+ "min_bid": 0.23,
+ "reliability": 0.994813,
+ "reliability2": 0.994813,
+ "geolocation": "Texas, US",
+ "verification": "deverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 53074074,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.2688888888888889,
+ "min_bid": 0.26666666666666666,
+ "reliability": 0.9511167,
+ "reliability2": 0.9511167,
+ "geolocation": "Texas, US",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 54080523,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.2688889222222222,
+ "min_bid": 0.2666667,
+ "reliability": 0.9659244,
+ "reliability2": 0.9659244,
+ "geolocation": "Texas, US",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 50687676,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.2696296629629629,
+ "min_bid": 0.2666667,
+ "reliability": 0.9831286,
+ "reliability2": 0.9831286,
+ "geolocation": "New Jersey, US",
+ "verification": "unverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 56202921,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.28222222222222226,
+ "min_bid": 0.28,
+ "reliability": 0.9928783,
+ "reliability2": 0.9928783,
+ "geolocation": "Hong Kong, HK",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58523997,
+ "gpu_name": "A100 SXM4",
+ "gpu_ram": 40960,
+ "num_gpus": 1,
+ "dph_total": 0.29555555555555557,
+ "min_bid": 0.29333333333333333,
+ "reliability": 0.9770653,
+ "reliability2": 0.9770653,
+ "geolocation": "Iowa, US",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58955859,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.32296296296296295,
+ "min_bid": 0.32,
+ "reliability": 0.9625633,
+ "reliability2": 0.9625633,
+ "geolocation": "Quebec, CA",
+ "verification": "deverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 37930682,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.3251851851851852,
+ "min_bid": 0.32,
+ "reliability": 0.9977738,
+ "reliability2": 0.9977738,
+ "geolocation": "Spain, ES",
+ "verification": "verified",
+ "storage_cost": 0.46666666666666673,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58868421,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.33555555555555555,
+ "min_bid": 0.3333333333333333,
+ "reliability": 0.9786792,
+ "reliability2": 0.9786792,
+ "geolocation": "United States, US",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59924610,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.3562222222222222,
+ "min_bid": 0.354,
+ "reliability": 0.9902221,
+ "reliability2": 0.9902221,
+ "geolocation": "Belgium, BE",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 57801635,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 49140,
+ "num_gpus": 1,
+ "dph_total": 0.3622222222222223,
+ "min_bid": 0.36000000000000004,
+ "reliability": 0.9880769,
+ "reliability2": 0.9880769,
+ "geolocation": "Thailand, TH",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 57527864,
+ "gpu_name": "A100 PCIE",
+ "gpu_ram": 40960,
+ "num_gpus": 1,
+ "dph_total": 0.3742222222222223,
+ "min_bid": 0.37200000000000005,
+ "reliability": 0.9643426,
+ "reliability2": 0.9643426,
+ "geolocation": "Japan, JP",
+ "verification": "verified",
+ "storage_cost": 0.20000000000000004,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59888567,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.3844148148148148,
+ "min_bid": 0.37333333333333335,
+ "reliability": 0.9968028,
+ "reliability2": 0.9968028,
+ "geolocation": "Taiwan, TW",
+ "verification": "verified",
+ "storage_cost": 0.9973333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 36643053,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.3925925925925926,
+ "min_bid": 0.38666666666666666,
+ "reliability": 0.9871033,
+ "reliability2": 0.9871033,
+ "geolocation": "United Kingdom, GB",
+ "verification": "deverified",
+ "storage_cost": 0.5333333333333332,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 25814725,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.39155555555555555,
+ "min_bid": 0.3893333333333333,
+ "reliability": 0.9976939,
+ "reliability2": 0.9976939,
+ "geolocation": "Iceland, IS",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59846906,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.40962962962962957,
+ "min_bid": 0.39999999999999997,
+ "reliability": 0.9845932,
+ "reliability2": 0.9845932,
+ "geolocation": "Beijing, CN",
+ "verification": "deverified",
+ "storage_cost": 0.8666666666666665,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59137888,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.40222222222222226,
+ "min_bid": 0.4,
+ "reliability": 0.9954978,
+ "reliability2": 0.9954978,
+ "geolocation": "Alberta, CA",
+ "verification": "verified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 50284377,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.41555555555555557,
+ "min_bid": 0.41333333333333333,
+ "reliability": 0.9116739,
+ "reliability2": 0.9116739,
+ "geolocation": "Minnesota, US",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59857072,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.4330074074074074,
+ "min_bid": 0.43226666666666663,
+ "reliability": 0.9637782,
+ "reliability2": 0.9637782,
+ "geolocation": "United States, US",
+ "verification": "deverified",
+ "storage_cost": 0.06666666666666665,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 38116562,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.4555555555555556,
+ "min_bid": 0.45333333333333337,
+ "reliability": 0.9976416,
+ "reliability2": 0.9976416,
+ "geolocation": "British Columbia, CA",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 51341862,
+ "gpu_name": "A100 SXM4",
+ "gpu_ram": 40960,
+ "num_gpus": 1,
+ "dph_total": 0.4718518518518518,
+ "min_bid": 0.4666666666666666,
+ "reliability": 0.7979845,
+ "reliability2": 0.7979845,
+ "geolocation": "Mississippi, US",
+ "verification": "verified",
+ "storage_cost": 0.46666666666666673,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58940647,
+ "gpu_name": "A100 SXM4",
+ "gpu_ram": 81920,
+ "num_gpus": 1,
+ "dph_total": 0.5222222222222223,
+ "min_bid": 0.52,
+ "reliability": 0.9847828,
+ "reliability2": 0.9847828,
+ "geolocation": "Austria, AT",
+ "verification": "verified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59830761,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.5222222222222223,
+ "min_bid": 0.52,
+ "reliability": 0.9810733,
+ "reliability2": 0.9810733,
+ "geolocation": "Guangdong, CN",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 57376615,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.5348148148148149,
+ "min_bid": 0.5333333333333333,
+ "reliability": 0.9799656,
+ "reliability2": 0.9799656,
+ "geolocation": "Taiwan, TW",
+ "verification": "deverified",
+ "storage_cost": 0.1333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58667289,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.5355555555555556,
+ "min_bid": 0.5333333333333333,
+ "reliability": 0.9898541,
+ "reliability2": 0.9898541,
+ "geolocation": "Thailand, TH",
+ "verification": "verified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 50734486,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.6022222222222222,
+ "min_bid": 0.6,
+ "reliability": 0.9924844,
+ "reliability2": 0.9924844,
+ "geolocation": "South Korea, KR",
+ "verification": "verified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59227354,
+ "gpu_name": "A100 SXM4",
+ "gpu_ram": 81920,
+ "num_gpus": 1,
+ "dph_total": 0.6586222222222222,
+ "min_bid": 0.6564,
+ "reliability": 0.9961462,
+ "reliability2": 0.9961462,
+ "geolocation": "United Arab Emirates, AE",
+ "verification": "unverified",
+ "storage_cost": 0.20000000000000004,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59796716,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 49140,
+ "num_gpus": 1,
+ "dph_total": 0.6703703703703703,
+ "min_bid": 0.6666666666666666,
+ "reliability": 0.9982841,
+ "reliability2": 0.9982841,
+ "geolocation": "Romania, RO",
+ "verification": "verified",
+ "storage_cost": 0.3333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 48704103,
+ "gpu_name": "H100 SXM",
+ "gpu_ram": 81559,
+ "num_gpus": 1,
+ "dph_total": 0.6740444444444444,
+ "min_bid": 0.6666666666666666,
+ "reliability": 0.9936286,
+ "reliability2": 0.9936286,
+ "geolocation": "Czechia, CZ",
+ "verification": "verified",
+ "storage_cost": 0.664,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59391499,
+ "gpu_name": "H100 PCIE",
+ "gpu_ram": 81559,
+ "num_gpus": 1,
+ "dph_total": 0.6743731555555555,
+ "min_bid": 0.6666666666666666,
+ "reliability": 0.9657309,
+ "reliability2": 0.9657309,
+ "geolocation": "South Africa, ZA",
+ "verification": "verified",
+ "storage_cost": 0.6935840000000001,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 56885480,
+ "gpu_name": "H100 PCIE",
+ "gpu_ram": 81559,
+ "num_gpus": 1,
+ "dph_total": 0.6707562518518518,
+ "min_bid": 0.6666666666666666,
+ "reliability": 0.9879379,
+ "reliability2": 0.9879379,
+ "geolocation": "South Africa, ZA",
+ "verification": "verified",
+ "storage_cost": 0.36806266666666665,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 30055164,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 49140,
+ "num_gpus": 1,
+ "dph_total": 0.7355555555555556,
+ "min_bid": 0.7333333333333334,
+ "reliability": 0.998608,
+ "reliability2": 0.998608,
+ "geolocation": "United Kingdom, GB",
+ "verification": "verified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59631467,
+ "gpu_name": "A100 SXM4",
+ "gpu_ram": 40960,
+ "num_gpus": 1,
+ "dph_total": 0.7355555555555556,
+ "min_bid": 0.7333333333333334,
+ "reliability": 0.9452575,
+ "reliability2": 0.9452575,
+ "geolocation": "California, US",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,604 @@
@@ -0,0 +1,604 @@
+{
+ "offers": [
+ {
+ "id": 56919557,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.09481481481481482,
+ "min_bid": 0.09333333333333334,
+ "reliability": 0.9734428,
+ "reliability2": 0.9734428,
+ "geolocation": "Ukraine, UA",
+ "verification": "unverified",
+ "storage_cost": 0.1333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 38849861,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.12222222222222223,
+ "min_bid": 0.09333333333333334,
+ "reliability": 0.9942635,
+ "reliability2": 0.9942635,
+ "geolocation": "Quebec, CA",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 55934598,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.13590148148148148,
+ "min_bid": 0.19999999999999998,
+ "reliability": 0.8926473,
+ "reliability2": 0.8926473,
+ "geolocation": "Texas, US",
+ "verification": "deverified",
+ "storage_cost": 0.1333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 35376208,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.14888888888888888,
+ "min_bid": 0.14666666666666667,
+ "reliability": 0.9747085,
+ "reliability2": 0.9747085,
+ "geolocation": "Guangxi Zhuangzu, CN",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59878525,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.17555555555555555,
+ "min_bid": 0.16,
+ "reliability": 0.7877959,
+ "reliability2": 0.7877959,
+ "geolocation": "California, US",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 57314311,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.19074074074074074,
+ "min_bid": 0.16,
+ "reliability": 0.9977229,
+ "reliability2": 0.9977229,
+ "geolocation": "Quebec, CA",
+ "verification": "deverified",
+ "storage_cost": 0.3666666666666667,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58393573,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.2022222222222222,
+ "min_bid": 0.09333333,
+ "reliability": 0.9888245,
+ "reliability2": 0.9888245,
+ "geolocation": "Kuwait, KW",
+ "verification": "verified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 36757236,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.20414814814814813,
+ "min_bid": 0.2,
+ "reliability": 0.99256,
+ "reliability2": 0.99256,
+ "geolocation": "Bulgaria, BG",
+ "verification": "verified",
+ "storage_cost": 0.1333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 19602471,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.21288888888888888,
+ "min_bid": 0.235,
+ "reliability": 0.9969887,
+ "reliability2": 0.9969887,
+ "geolocation": "South Korea, KR",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 55957532,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.21629629629629632,
+ "min_bid": 0.16,
+ "reliability": 0.9938383,
+ "reliability2": 0.9938383,
+ "geolocation": "Quebec, CA",
+ "verification": "deverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 9817760,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.23232592592592594,
+ "min_bid": 0.22,
+ "reliability": 0.9991736,
+ "reliability2": 0.9991736,
+ "geolocation": "Ireland, IE",
+ "verification": "deverified",
+ "storage_cost": 0.14933333333333335,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 55359923,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.32222222222222224,
+ "min_bid": 0.27999999999999997,
+ "reliability": 0.9899092,
+ "reliability2": 0.9899092,
+ "geolocation": "Hong Kong, HK",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59612506,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 16376,
+ "num_gpus": 1,
+ "dph_total": 0.33629629629629626,
+ "min_bid": 0.1333333,
+ "reliability": 0.9353087,
+ "reliability2": 0.9353087,
+ "geolocation": "California, US",
+ "verification": "unverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 46773554,
+ "gpu_name": "A100 PCIE",
+ "gpu_ram": 40960,
+ "num_gpus": 1,
+ "dph_total": 0.337037037037037,
+ "min_bid": 0.3333333333333333,
+ "reliability": 0.9963616,
+ "reliability2": 0.9963616,
+ "geolocation": "Japan, JP",
+ "verification": "deverified",
+ "storage_cost": 0.3333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59554534,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.36888888888888893,
+ "min_bid": 0.13333333333333333,
+ "reliability": 0.9769943,
+ "reliability2": 0.9769943,
+ "geolocation": "Czechia, CZ",
+ "verification": "verified",
+ "storage_cost": 0.20000000000000004,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59877086,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.3743703703703704,
+ "min_bid": 0.224,
+ "reliability": 0.9731478,
+ "reliability2": 0.9731478,
+ "geolocation": "Vietnam, VN",
+ "verification": "verified",
+ "storage_cost": 0.3333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 37930682,
+ "gpu_name": "RTX 3090",
+ "gpu_ram": 24576,
+ "num_gpus": 1,
+ "dph_total": 0.37851851851851853,
+ "min_bid": 0.32,
+ "reliability": 0.9977738,
+ "reliability2": 0.9977738,
+ "geolocation": "Spain, ES",
+ "verification": "verified",
+ "storage_cost": 0.46666666666666673,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 25814727,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.4022222222222222,
+ "min_bid": 0.3893333333333333,
+ "reliability": 0.9976939,
+ "reliability2": 0.9976939,
+ "geolocation": "Iceland, IS",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 56063498,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.4022222222222222,
+ "min_bid": 0.39999999999999997,
+ "reliability": 0.9914165,
+ "reliability2": 0.9914165,
+ "geolocation": "New York, US",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 57004718,
+ "gpu_name": "A100 SXM4",
+ "gpu_ram": 40960,
+ "num_gpus": 1,
+ "dph_total": 0.4029629629629629,
+ "min_bid": 0.39999999999999997,
+ "reliability": 0.9873088,
+ "reliability2": 0.9873088,
+ "geolocation": "California, US",
+ "verification": "deverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 56141036,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.44296296296296295,
+ "min_bid": 0.21333333333333335,
+ "reliability": 0.9533946,
+ "reliability2": 0.9533946,
+ "geolocation": "Quebec, CA",
+ "verification": "deverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59749210,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.4562962962962963,
+ "min_bid": 0.45333333333333337,
+ "reliability": 0.8993912,
+ "reliability2": 0.8993912,
+ "geolocation": "The Netherlands, NL",
+ "verification": "unverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59884402,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.462962962962963,
+ "min_bid": 0.39999999999999997,
+ "reliability": 0.9903628,
+ "reliability2": 0.9903628,
+ "geolocation": "Beijing, CN",
+ "verification": "deverified",
+ "storage_cost": 0.8666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 38116562,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.46888888888888886,
+ "min_bid": 0.45333333333333337,
+ "reliability": 0.9976416,
+ "reliability2": 0.9976416,
+ "geolocation": "British Columbia, CA",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59808361,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.47111111111111104,
+ "min_bid": 0.4666666666666666,
+ "reliability": 0.9959959,
+ "reliability2": 0.9959959,
+ "geolocation": "Arizona, US",
+ "verification": "verified",
+ "storage_cost": 0.39999999999999997,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 36643053,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.47259259259259256,
+ "min_bid": 0.38666666666666666,
+ "reliability": 0.9871033,
+ "reliability2": 0.9871033,
+ "geolocation": "United Kingdom, GB",
+ "verification": "deverified",
+ "storage_cost": 0.5333333333333332,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58523997,
+ "gpu_name": "A100 SXM4",
+ "gpu_ram": 40960,
+ "num_gpus": 1,
+ "dph_total": 0.4822222222222223,
+ "min_bid": 0.29333333333333333,
+ "reliability": 0.9770653,
+ "reliability2": 0.9770653,
+ "geolocation": "Iowa, US",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 50103798,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.48414814814814816,
+ "min_bid": 0.496,
+ "reliability": 0.9960525,
+ "reliability2": 0.9960525,
+ "geolocation": "Texas, US",
+ "verification": "deverified",
+ "storage_cost": 0.1333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 48882881,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 49140,
+ "num_gpus": 1,
+ "dph_total": 0.48888888888888893,
+ "min_bid": 0.39999999999999997,
+ "reliability": 0.9914976,
+ "reliability2": 0.9914976,
+ "geolocation": "California, US",
+ "verification": "verified",
+ "storage_cost": 0.7999999999999999,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58382376,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.497037037037037,
+ "min_bid": 0.6666666666666666,
+ "reliability": 0.9884196,
+ "reliability2": 0.9884196,
+ "geolocation": "South Korea, KR",
+ "verification": "verified",
+ "storage_cost": 0.3333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 50967859,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.497037037037037,
+ "min_bid": 0.6666666666666666,
+ "reliability": 0.9906751,
+ "reliability2": 0.9906751,
+ "geolocation": "South Korea, KR",
+ "verification": "verified",
+ "storage_cost": 0.3333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 57994723,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.5044444444444444,
+ "min_bid": 0.49333333333333335,
+ "reliability": 0.9870163,
+ "reliability2": 0.9870163,
+ "geolocation": "Poland, PL",
+ "verification": "unverified",
+ "storage_cost": 1,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59618563,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.5125925925925925,
+ "min_bid": 0.326,
+ "reliability": 0.9823852,
+ "reliability2": 0.9823852,
+ "geolocation": "California, US",
+ "verification": "verified",
+ "storage_cost": 0.5333333333333332,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58803461,
+ "gpu_name": "A100 PCIE",
+ "gpu_ram": 40960,
+ "num_gpus": 1,
+ "dph_total": 0.5355555555555556,
+ "min_bid": 0.5333333333333333,
+ "reliability": 0.9601313,
+ "reliability2": 0.9601313,
+ "geolocation": "Japan, JP",
+ "verification": "unverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59830763,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.5355555555555556,
+ "min_bid": 0.52,
+ "reliability": 0.9810733,
+ "reliability2": 0.9810733,
+ "geolocation": "Guangdong, CN",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59937771,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.5392592592592592,
+ "min_bid": 0.3466666666666667,
+ "reliability": 0.9749228,
+ "reliability2": 0.9749228,
+ "geolocation": "California, US",
+ "verification": "unverified",
+ "storage_cost": 0.5333333333333332,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 59524409,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 49140,
+ "num_gpus": 1,
+ "dph_total": 0.5484444444444444,
+ "min_bid": 0.3333333333333333,
+ "reliability": 0.9602438,
+ "reliability2": 0.9602438,
+ "geolocation": "Sichuan, CN",
+ "verification": "unverified",
+ "storage_cost": 0.16000000000000003,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 57376611,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.5614814814814816,
+ "min_bid": 0.5333333333333333,
+ "reliability": 0.9799656,
+ "reliability2": 0.9799656,
+ "geolocation": "Taiwan, TW",
+ "verification": "deverified",
+ "storage_cost": 0.1333333333333333,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 58120176,
+ "gpu_name": "RTX 4090",
+ "gpu_ram": 24564,
+ "num_gpus": 1,
+ "dph_total": 0.5749629629629629,
+ "min_bid": 0.23,
+ "reliability": 0.994813,
+ "reliability2": 0.994813,
+ "geolocation": "Texas, US",
+ "verification": "deverified",
+ "storage_cost": 0.2666666666666666,
+ "is_bid": false,
+ "rentable": true
+ },
+ {
+ "id": 53074074,
+ "gpu_name": "RTX 5090",
+ "gpu_ram": 32607,
+ "num_gpus": 1,
+ "dph_total": 0.6022222222222222,
+ "min_bid": 0.26666666666666666,
+ "reliability": 0.9511167,
+ "reliability2": 0.9511167,
+ "geolocation": "Texas, US",
+ "verification": "deverified",
+ "storage_cost": 0.19999999999999998,
+ "is_bid": false,
+ "rentable": true
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,178 @@
@@ -0,0 +1,178 @@
+'use strict';
+// The public GPU marketplace price reader: saved provider pages, the cache that survives a failed source, and
+// the per-token cost implied by the catalog's throughput next to the cheapest comparable API price. No test
+// reaches the internet: every source is a saved page from test/fixtures/gpu-market/.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { boot, request, token, suite } = require('./helpers');
+const { testDb } = require('./db');
+const marketLib = require('../server/gpu/market');
+const { tokenCosts, cheapestComparable } = require('../server/gpu/token-cost');
+const catalog = require('../server/gpu/catalog');
+
+const FIXTURES = path.join(__dirname, 'fixtures', 'gpu-market');
+const fixture = (name) => fs.readFileSync(path.join(FIXTURES, name), 'utf8');
+
+/** The saved pages, behind the same fetch seam the service uses. Vast is called twice (on demand, then bid). */
+function fixtureFetch({ fail = null } = {}) {
+ let vastCalls = 0;
+ return async (url) => {
+ if (fail && String(url).includes(fail)) throw new Error('saved source is down');
+ let name;
+ if (String(url).includes('vast.ai')) name = vastCalls++ === 0 ? 'vast-ondemand.json' : 'vast-bid.json';
+ else if (String(url).includes('runpod.io')) name = 'runpod-pricing.html';
+ else if (String(url).includes('salad.com')) name = 'salad-pricing.html';
+ else if (String(url).includes('tensordock.com')) name = 'tensordock-pricing.html';
+ else if (String(url).includes('cloudrift.ai')) name = 'cloudrift-pricing.html';
+ else throw new Error(`no saved fixture for ${url}`);
+ const text = fixture(name);
+ return { ok: true, status: 200, text: async () => text };
+ };
+}
+
+const t = suite('gpu-market');
+let h;
+let db;
+let now = Date.parse('2026-11-09T20:00:00Z');
+let market;
+let goodReport;
+
+t.test('the saved provider pages parse into the same reading shape', async () => {
+ const onDemand = marketLib.parseVast(fixture('vast-ondemand.json'), 'on_demand');
+ const bid = marketLib.parseVast(fixture('vast-bid.json'), 'spot');
+ assert.ok(onDemand.length > 0 && bid.length > 0);
+ assert.equal(marketLib.targetFor('A100 PCIE', 40960), null, 'a 40 GB A100 is not the 80 GB row');
+ assert.equal(marketLib.targetFor('A100 SXM4', 81920).gpu, 'A100 80GB');
+ const lowest = (rows, key, kind) => marketLib.lowest(rows).filter((r) => r.gpu_key === key && r.kind === kind)
+ .sort((a, b) => a.hourly_usd - b.hourly_usd)[0];
+ const vast4090 = lowest(onDemand, 'rtx-4090', 'on_demand');
+ assert.equal(vast4090.hourly_usd, 0.13590148148148148);
+ assert.equal(vast4090.region, 'Texas, US');
+ assert.ok(vast4090.reliability > 0 && vast4090.reliability <= 1);
+ assert.match(vast4090.note, /storage/);
+ assert.equal(lowest(bid, 'rtx-4090', 'spot').hourly_usd, 0.1466667);
+ const offer = { num_gpus: 1, gpu_name: 'RTX 4090', gpu_ram: 24576, dph_total: 1 };
+ for (const reliability of [null, undefined, '']) {
+ assert.strictEqual(marketLib.parseVast({ offers: [{ ...offer, reliability }] }, 'on_demand')[0].reliability, null);
+ }
+ assert.strictEqual(marketLib.parseVast({ offers: [{ ...offer, reliability: 0 }] }, 'on_demand')[0].reliability, 0);
+
+ const runpod = marketLib.lowest(marketLib.parseRunpod(fixture('runpod-pricing.html')));
+ assert.equal(runpod.find((r) => r.gpu_key === 'h100' && r.kind === 'on_demand').hourly_usd, 1.99);
+ assert.ok(runpod.every((r) => r.kind === 'on_demand'), 'Community Cloud is not a spot quote');
+ assert.equal(runpod.find((r) => r.gpu_key === 'rtx-4090' && r.kind === 'on_demand').hourly_usd, 0.34);
+
+ const salad = marketLib.lowest(marketLib.parseSalad(fixture('salad-pricing.html')));
+ assert.equal(salad.find((r) => r.gpu_key === 'rtx-4090' && r.kind === 'on_demand').hourly_usd, 0.44);
+ assert.equal(salad.find((r) => r.gpu_key === 'rtx-4090' && r.kind === 'spot').hourly_usd, 0.16);
+ assert.match(salad.find((r) => r.gpu_key === 'rtx-4090' && r.kind === 'spot').note, /interruptible/);
+
+ const tensordock = marketLib.parseTensordock(fixture('tensordock-pricing.html'));
+ assert.equal(tensordock.find((r) => r.gpu_key === 'h100').hourly_usd, 2.25);
+ assert.equal(tensordock.find((r) => r.gpu_key === 'a100-80gb').hourly_usd, 1.8);
+ assert.equal(tensordock.some((r) => r.kind === 'spot'), false, 'TensorDock publishes no spot floor on its public page');
+
+ const cloudrift = marketLib.parseCloudrift(fixture('cloudrift-pricing.html'));
+ assert.equal(cloudrift.find((r) => r.gpu_key === 'rtx-4090').hourly_usd, 0.45);
+ assert.equal(cloudrift.find((r) => r.gpu_key === 'rtx-5090').hourly_usd, 0.65);
+ assert.equal(cloudrift.some((r) => r.gpu_key === 'h100'), false, 'CloudRift quotes H100 on demand on request');
+});
+
+t.test('the cache keeps the lowest reading per provider, card and kind', async () => {
+ db = await testDb();
+ market = marketLib.createGpuMarket({ db: db.db, config: { gpuMarket: { enabled: false } }, clock: { now: () => now } });
+ const out = await market.refresh({ fetchImpl: fixtureFetch() });
+ assert.equal(out.refreshed, 5);
+ assert.equal(out.failed, 0);
+ goodReport = await market.report();
+ assert.equal(goodReport.sources.length, 5);
+ assert.ok(goodReport.sources.every((s) => s.status === 'ok' && s.stale === false));
+ const g = (key) => goodReport.gpus.find((x) => x.key === key);
+ assert.equal(g('rtx-4090').on_demand.provider, 'vast');
+ assert.equal(g('rtx-4090').on_demand.hourly_usd, 0.13590148148148148);
+ assert.equal(g('rtx-4090').spot.hourly_usd, 0.1466667);
+ assert.equal(g('h100').on_demand.provider, 'runpod');
+ assert.equal(g('h100').spot.provider, 'vast');
+ assert.equal(g('h100').spot.hourly_usd, 0.6666666666666666);
+ assert.ok(g('h200').on_demand);
+ assert.strictEqual(g('h200').spot, null, 'the source has no H200 spot quote');
+ assert.equal(await market.due(), false, 'a fresh read is not due again');
+ now += 7 * 60 * 60 * 1000;
+ assert.equal(await market.due(), true, 'the six-hour interval makes the next read due');
+ now -= 7 * 60 * 60 * 1000;
+});
+
+t.test('a source that fails keeps its last reading, marked stale', async () => {
+ const before = goodReport.gpus.find((x) => x.key === 'h200').on_demand;
+ const okAt = goodReport.sources.find((s) => s.provider === 'runpod').ok_at;
+ now += 10 * 60 * 1000;
+ const out = await market.refresh({ fetchImpl: fixtureFetch({ fail: 'runpod.io' }) });
+ assert.equal(out.failed, 1);
+ assert.equal(out.refreshed, 4);
+ const report = await market.report();
+ const runpod = report.sources.find((s) => s.provider === 'runpod');
+ assert.equal(runpod.status, 'failed');
+ assert.equal(runpod.stale, true);
+ assert.equal(runpod.ok_at, okAt);
+ assert.match(runpod.error, /saved source is down/);
+ const h200 = report.gpus.find((x) => x.key === 'h200');
+ assert.equal(h200.on_demand.provider, before.provider);
+ assert.equal(h200.on_demand.hourly_usd, before.hourly_usd);
+ assert.equal(h200.on_demand.stale, true);
+ assert.ok(h200.readings.length > 0, 'the last rows were not deleted');
+ const stale = structuredClone(goodReport);
+ for (const gpu of stale.gpus) for (const reading of gpu.readings) reading.stale = true;
+ assert.ok(tokenCosts({ market: stale, catalog }).every((row) => !row.is_cheapest), 'stale readings cannot claim the fresh price floor');
+});
+
+t.test('the implied per-token cost uses the catalog throughput and the cheapest comparable API', async () => {
+ const models = [
+ { provider_key: 'deepseek', model_key: 'deepseek-v4-flash', display_name: 'DeepSeek V4 Flash', family: 'deepseek', uncensored: false, cost: { in_per_mtok: 0.037, out_per_mtok: 0.17 } },
+ { provider_key: 'refuseless', model_key: 'deepseek-v4.1-flash-abliterated', display_name: 'DeepSeek V4.1 Flash (abliterated)', family: 'deepseek', uncensored: true, cost: { in_per_mtok: null, out_per_mtok: 0.5 } },
+ { provider_key: 'openai', model_key: 'gpt-5-nano', display_name: 'GPT-5 nano', family: 'gpt', uncensored: false, cost: { in_per_mtok: 0.05, out_per_mtok: 0.4 } },
+ ];
+ // RunPod is stale from the previous test; the 3090 and 4090 rows are Vast and fresh.
+ const rows = tokenCosts({ market: goodReport, models, catalog });
+ assert.equal(rows.length, 22, 'both models, both fitting cards, both kinds and every provider reading');
+ const row = (key, gpu, kind, provider) => rows.find((r) => r.model_key === key && r.gpu === gpu && r.kind === kind && r.provider === provider);
+ const p = row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'vast');
+ assert.equal(p.is_cheapest, true);
+ assert.ok(row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'salad'), 'the Salad 4090 hour is beside Vast');
+ assert.equal(row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'cloudrift').hourly_usd, 0.45);
+ assert.equal(row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'cloudrift').is_cheapest, false);
+ assert.equal(p.hourly_usd, 0.13590148148148148);
+ assert.equal(p.throughput_tokens_per_hour, 180000);
+ assert.match(p.throughput_source, /catalog planning estimate/);
+ assert.equal(p.usd_per_million_tokens, 0.755008);
+ assert.equal(p.cheapest_api.model_key, 'deepseek-v4-flash');
+ assert.equal(p.cheapest_api.match, 'same_family_weights');
+ const a = row('deepseek-r1-32b-abliterated', 'RTX 4090', 'on_demand', 'vast');
+ assert.equal(a.cheapest_api.model_key, 'deepseek-v4.1-flash-abliterated', 'the abliterated comparison prefers the same weights');
+ assert.equal(a.cheapest_api.match, 'same_family_weights');
+ assert.equal(cheapestComparable({ key: 'a', family: 'none', uncensored: false }, models), null);
+});
+
+t.test('the admin API exposes both to the GPUs and supply console sections', async () => {
+ h = await boot();
+ await h.gpuMarket.refresh({ fetchImpl: fixtureFetch() });
+ const read = token('console', ['ai.usage.read']);
+ const gpus = await request(h.base, 'GET', '/api/v1/admin/console/gpus', { tok: read });
+ assert.equal(gpus.status, 200, gpus.text);
+ assert.equal(gpus.body.market.sources.length, 5);
+ assert.equal(gpus.body.token_costs.length, 22);
+ assert.ok(gpus.body.state && Array.isArray(gpus.body.state.models));
+ const supply = await request(h.base, 'GET', '/api/v1/admin/console/supply', { tok: read });
+ assert.equal(supply.status, 200, supply.text);
+ assert.equal(supply.body.dry_run, true);
+ assert.equal(supply.body.market.read_at, gpus.body.market.read_at);
+ assert.equal(supply.body.token_costs.length, 22);
+});
+
+t.test('close', async () => {
+ if (h) await h.stop();
+ if (db) await db.close();
+ process.stdout.write('gpu-market: saved pages, stale cache, implied token costs and both admin sections passed\n');
+});
+
+t.run();