openvibers/openvibe.ai Commits

Read public GPU marketplace prices and the token cost they imply (#406)

OP OpenVibers committed 4 days ago · 55ce86d1f5 · 18 files, +2254 −9

* Read public GPU marketplace prices and the token cost they imply

* Replay every own price source in the market fixture test

* Renumber the GPU market migration above the open provider circuits PR

* Keep GPU price kinds, missing reliability and stale floors accurate

* Merge main and retain exact provider route market tests

Changes

ddiff --git a/docs/ai-gpus.md b/docs/ai-gpus.md
iindex 5e6527c..7106868 100644
---- a/docs/ai-gpus.md
++++ b/docs/ai-gpus.md
@@@ -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
ddiff --git a/docs/exchange-platform.md b/docs/exchange-platform.md
iindex e9ec27c..827a6d3 100644
---- a/docs/exchange-platform.md
++++ b/docs/exchange-platform.md
@@@ -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
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.
## Public market observations
ddiff --git a/migrations/0061_gpu_market_prices.sql b/migrations/0061_gpu_market_prices.sql
nnew file mode 100644
iindex 0000000..2a58a1b
---- /dev/null
++++ b/migrations/0061_gpu_market_prices.sql
@@@ -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
++);
ddiff --git a/server/api/console-data.js b/server/api/console-data.js
iindex f5eb634..dd491f4 100644
---- a/server/api/console-data.js
++++ b/server/api/console-data.js
@@@ -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) => {
ddiff --git a/server/app.js b/server/app.js
iindex ab1a503..b7f1b13 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -22,7 +22,7 @@ const pkg = require('../package.json');
const { adminRoutes } = require('openvibe-shared/config');
const ownerInputs = require('./owner-inputs');
--function createApp({ choices = null, searchFetch = null, config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null, preferences = null, grants = null, vault = null, settings = null, benchEvents = null, runBudgets = null, memory = null, memoryImport = null, memoryServices = null, memorySharing = null, community = null, endpoints = null, webhooks = null, presets = null, receipts = null, balance = null, creditGrants = null, sessionCookies = null, gpus = null, 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).
ddiff --git a/server/config.js b/server/config.js
iindex ffba651..b21d9de 100644
---- a/server/config.js
++++ b/server/config.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).
ddiff --git a/server/gpu/catalog.js b/server/gpu/catalog.js
iindex c1f60f6..9b29e7b 100644
---- a/server/gpu/catalog.js
++++ b/server/gpu/catalog.js
@@@ -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 },
ddiff --git a/server/gpu/market.js b/server/gpu/market.js
nnew file mode 100644
iindex 0000000..aa74a8d
---- /dev/null
++++ b/server/gpu/market.js
@@@ -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(/&nbsp;/g, ' ').replace(/&amp;/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(/&nbsp;/g, ' ').replace(/&amp;/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,
++};
ddiff --git a/server/gpu/token-cost.js b/server/gpu/token-cost.js
nnew file mode 100644
iindex 0000000..3b04b2a
---- /dev/null
++++ b/server/gpu/token-cost.js
@@@ -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 };
ddiff --git a/server/index.js b/server/index.js
iindex 68dae4f..9f59ccc 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -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) {
ddiff --git a/test/exchange-market.test.js b/test/exchange-market.test.js
iindex 165ba84..3ed6356 100644
---- a/test/exchange-market.test.js
++++ b/test/exchange-market.test.js
@@@ -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;
ddiff --git a/test/fixtures/gpu-market/cloudrift-pricing.html b/test/fixtures/gpu-market/cloudrift-pricing.html
nnew file mode 100644
iindex 0000000..6aa1196
---- /dev/null
++++ b/test/fixtures/gpu-market/cloudrift-pricing.html
@@@ -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>
ddiff --git a/test/fixtures/gpu-market/runpod-pricing.html b/test/fixtures/gpu-market/runpod-pricing.html
nnew file mode 100644
iindex 0000000..11fe0a8
---- /dev/null
++++ b/test/fixtures/gpu-market/runpod-pricing.html
@@@ -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>
ddiff --git a/test/fixtures/gpu-market/salad-pricing.html b/test/fixtures/gpu-market/salad-pricing.html
nnew file mode 100644
iindex 0000000..3ae354b
---- /dev/null
++++ b/test/fixtures/gpu-market/salad-pricing.html
@@@ -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>
ddiff --git a/test/fixtures/gpu-market/tensordock-pricing.html b/test/fixtures/gpu-market/tensordock-pricing.html
nnew file mode 100644
iindex 0000000..9896ce6
---- /dev/null
++++ b/test/fixtures/gpu-market/tensordock-pricing.html
@@@ -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>
ddiff --git a/test/fixtures/gpu-market/vast-bid.json b/test/fixtures/gpu-market/vast-bid.json
nnew file mode 100644
iindex 0000000..6f96295
---- /dev/null
++++ b/test/fixtures/gpu-market/vast-bid.json
@@@ -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
++ }
++ ]
++}
ddiff --git a/test/fixtures/gpu-market/vast-ondemand.json b/test/fixtures/gpu-market/vast-ondemand.json
nnew file mode 100644
iindex 0000000..479472a
---- /dev/null
++++ b/test/fixtures/gpu-market/vast-ondemand.json
@@@ -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
++ }
++ ]
++}
ddiff --git a/test/gpu-market.test.js b/test/gpu-market.test.js
nnew file mode 100644
iindex 0000000..8ce1514
---- /dev/null
++++ b/test/gpu-market.test.js
@@@ -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();