diff --git a/deploy/mainnet/docker-compose.surplus-seller.yml b/deploy/mainnet/docker-compose.surplus-seller.yml index 3f35a15..beccbcc 100644 --- a/deploy/mainnet/docker-compose.surplus-seller.yml +++ b/deploy/mainnet/docker-compose.surplus-seller.yml @@ -22,6 +22,20 @@ services: - no-new-privileges:true volumes: - surplus-seller-state:/var/lib/surplus-manager + networks: + - traefik-net + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik-net" + - "traefik.http.routers.mp-openrouter-reseller-mainnet.entrypoints=web" + - "traefik.http.routers.mp-openrouter-reseller-mainnet.rule=Host(`mp-suppliers-openrouter-reseller.vector.apexfusion.org`)" + - "traefik.http.routers.mp-openrouter-reseller-mainnet.middlewares=mp-openrouter-reseller-mainnet-https" + - "traefik.http.middlewares.mp-openrouter-reseller-mainnet-https.redirectscheme.scheme=https" + - "traefik.http.middlewares.mp-openrouter-reseller-mainnet-https.redirectscheme.permanent=true" + - "traefik.http.routers.mp-openrouter-reseller-mainnet-tls.entrypoints=websecure" + - "traefik.http.routers.mp-openrouter-reseller-mainnet-tls.rule=Host(`mp-suppliers-openrouter-reseller.vector.apexfusion.org`)" + - "traefik.http.routers.mp-openrouter-reseller-mainnet-tls.tls.certresolver=letsencrypt" + - "traefik.http.services.mp-openrouter-reseller-mainnet.loadbalancer.server.port=8080" healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null || exit 1"] interval: 20s @@ -37,3 +51,8 @@ services: volumes: surplus-seller-state: name: marketplace-mainnet-surplus-seller-state + +networks: + traefik-net: + external: true + name: traefik-net diff --git a/supplier/src/cli/surplus-seller.ts b/supplier/src/cli/surplus-seller.ts index ea0a378..5c16e5c 100644 --- a/supplier/src/cli/surplus-seller.ts +++ b/supplier/src/cli/surplus-seller.ts @@ -1,4 +1,9 @@ -import { createServer, type Server } from "node:http"; +import { + createServer, + type IncomingMessage, + type Server, + type ServerResponse, +} from "node:http"; import { fileURLToPath } from "node:url"; import { resolve } from "node:path"; import { loadSurplusManagerConfig } from "../surplus/config.js"; @@ -6,6 +11,11 @@ import { SurplusClient } from "../surplus/client.js"; import { SurplusSellerController } from "../surplus/controller.js"; import { OpenRouterKeyClient } from "../surplus/openRouterKey.js"; import { FileSurplusStateStore } from "../surplus/state.js"; +import { + SurplusDashboardService, + type SurplusDashboardSnapshot, +} from "../surplus/dashboard.js"; +import { renderSurplusDashboardPage } from "../surplus/dashboardPage.js"; export async function runSurplusSeller( env: Record, @@ -29,7 +39,13 @@ export async function runSurplusSeller( stateStore, log: (message) => console.log(message), }); - const server = createStatusServer(controller); + const dashboard = new SurplusDashboardService({ + controller, + client, + allowance, + config, + }); + const server = createSurplusServer(controller, dashboard); await new Promise((resolveListen, rejectListen) => { server.once("error", rejectListen); server.listen(config.port, "0.0.0.0", () => { @@ -41,26 +57,81 @@ export async function runSurplusSeller( return { server, controller }; } -function createStatusServer(controller: SurplusSellerController): Server { +interface DashboardSource { + getSnapshot(): Promise; +} + +export function createSurplusServer( + controller: Pick, + dashboard: DashboardSource, +): Server { return createServer((request, response) => { - const status = controller.snapshot(); - if (request.method === "GET" && request.url === "/healthz") { - const healthy = controller.healthy(); - response.statusCode = healthy ? 200 : 503; - response.setHeader("content-type", "application/json"); - response.end(JSON.stringify({ ok: healthy, phase: status.phase })); - return; - } - if (request.method === "GET" && request.url === "/status") { + void handleRequest(controller, dashboard, request, response); + }); +} + +async function handleRequest( + controller: Pick, + dashboard: DashboardSource, + request: IncomingMessage, + response: ServerResponse, +): Promise { + const path = request.url?.split("?")[0] ?? "/"; + response.setHeader("cache-control", "no-store"); + response.setHeader("x-content-type-options", "nosniff"); + response.setHeader("x-frame-options", "DENY"); + response.setHeader("referrer-policy", "no-referrer"); + if (request.method === "GET" && path === "/") { + response.statusCode = 302; + response.setHeader("location", "/reseller"); + response.end(); + return; + } + if ( + request.method === "GET" && + (path === "/reseller" || path === "/reseller/") + ) { + response.statusCode = 200; + response.setHeader("content-type", "text/html; charset=utf-8"); + response.setHeader( + "content-security-policy", + "default-src 'self'; style-src 'self' 'unsafe-inline'; " + + "script-src 'self' 'unsafe-inline'; connect-src 'self'; " + + "img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'", + ); + response.end(renderSurplusDashboardPage()); + return; + } + const status = controller.snapshot(); + if (request.method === "GET" && path === "/healthz") { + const healthy = controller.healthy(); + response.statusCode = healthy ? 200 : 503; + response.setHeader("content-type", "application/json"); + response.end(JSON.stringify({ ok: healthy, phase: status.phase })); + return; + } + if (request.method === "GET" && path === "/status") { + response.statusCode = 200; + response.setHeader("content-type", "application/json"); + response.end(JSON.stringify(status)); + return; + } + if (request.method === "GET" && path === "/dashboard/api") { + try { + const snapshot = await dashboard.getSnapshot(); response.statusCode = 200; response.setHeader("content-type", "application/json"); - response.end(JSON.stringify(status)); - return; + response.end(JSON.stringify(snapshot)); + } catch { + response.statusCode = 503; + response.setHeader("content-type", "application/json"); + response.end(JSON.stringify({ error: "dashboard_data_unavailable" })); } - response.statusCode = 404; - response.setHeader("content-type", "application/json"); - response.end(JSON.stringify({ error: "not_found" })); - }); + return; + } + response.statusCode = 404; + response.setHeader("content-type", "application/json"); + response.end(JSON.stringify({ error: "not_found" })); } export async function main( diff --git a/supplier/src/surplus/client.ts b/supplier/src/surplus/client.ts index 976451d..429e4fa 100644 --- a/supplier/src/surplus/client.ts +++ b/supplier/src/surplus/client.ts @@ -40,6 +40,9 @@ export interface SurplusOrderBookOffer { healthy: boolean; trusted: boolean; trades24h: number; + rank: number; + capRemainingMicroUsd: number | null; + volume24hMicroUsd: number; } export interface SurplusOrderBook { @@ -64,17 +67,48 @@ export interface SurplusOfferPatch { } export interface SurplusSale { + id: string; model: string; offerId: string | null; settlementStatus: string; createdAt: string | null; sellerCostMicroUsd: number; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + effectiveInputUsdPer1m: number; + effectiveOutputUsdPer1m: number; + transactionHash: string | null; +} + +export interface SurplusDailyEarnings { + day: string; + earnedUsd: number; + inputTokens: number; + outputTokens: number; + requests: number; + totalTokens: number; +} + +export interface SurplusModelEarnings { + model: string; + earnedUsd: number; + requests: number; + totalTokens: number; + lastSaleAt: string | null; } export interface SurplusEarnings { totalEarnedMicroUsd: number; pendingMicroUsd: number; paidMicroUsd: number; + daily: SurplusDailyEarnings[]; + byModel: SurplusModelEarnings[]; + requestCount: number; + tokenCount: number; + topModel: string | null; + payoutHoldReason: string | null; + payoutHoldReleasesAt: string | null; recentSales: SurplusSale[]; } @@ -263,15 +297,43 @@ export class SurplusClient { async getEarnings(): Promise { const body = await this.requestJson("/v1/seller/earnings?range=lifetime"); const root = record(body, "Surplus earnings response"); - const rawSales = root.recent_sales; - if (!Array.isArray(rawSales)) { + if (!Array.isArray(root.recent_sales)) { throw new Error("Surplus earnings response.recent_sales must be an array"); } + if (!Array.isArray(root.daily)) { + throw new Error("Surplus earnings response.daily must be an array"); + } + if (!Array.isArray(root.by_model)) { + throw new Error("Surplus earnings response.by_model must be an array"); + } + const share = record(root.share, "Surplus earnings response.share"); + const payoutHold = optionalRecord(root.payout_hold); return { totalEarnedMicroUsd: microUsd(root.total_earned_usdc, "total_earned_usdc"), pendingMicroUsd: microUsd(root.pending_usdc, "pending_usdc"), paidMicroUsd: microUsd(root.paid_usdc, "paid_usdc"), - recentSales: rawSales.map((sale, index) => parseSale(sale, index)), + daily: root.daily.map((day, index) => parseDailyEarnings(day, index)), + byModel: root.by_model.map((model, index) => + parseModelEarnings(model, index) + ), + requestCount: nonNegativeSafeInteger( + share.requests, + "Surplus earnings share.requests", + ), + tokenCount: nonNegativeSafeInteger( + share.tokens, + "Surplus earnings share.tokens", + ), + topModel: optionalString(share.top_model), + payoutHoldReason: payoutHold + ? optionalString(payoutHold.reason) + : null, + payoutHoldReleasesAt: payoutHold + ? optionalTimestamp(payoutHold.releases_at, "payout_hold.releases_at") + : null, + recentSales: root.recent_sales.map((sale, index) => + parseSale(sale, index) + ), }; } @@ -460,17 +522,98 @@ function parseOrderBookOffer(value: unknown, index: number): SurplusOrderBookOff healthy: boolean(item.healthy, `Surplus order book offer ${index}.healthy`), trusted: boolean(item.trusted, `Surplus order book offer ${index}.trusted`), trades24h: nonNegativeSafeInteger(item.trades_24h, `Surplus order book offer ${index}.trades_24h`), + rank: positiveSafeInteger(item.rank, `Surplus order book offer ${index}.rank`), + capRemainingMicroUsd: optionalMicroUsd( + item.cap_remaining, + `Surplus order book offer ${index}.cap_remaining`, + ), + volume24hMicroUsd: microUsd( + item.volume_24h, + `Surplus order book offer ${index}.volume_24h`, + ), }; } function parseSale(value: unknown, index: number): SurplusSale { const item = record(value, `Surplus sale ${index}`); return { + id: string(item.id, `Surplus sale ${index}.id`), model: string(item.model, `Surplus sale ${index}.model`), offerId: optionalString(item.offer_id), settlementStatus: string(item.settlement_status, `Surplus sale ${index}.settlement_status`), - createdAt: optionalString(item.created_at), + createdAt: optionalTimestamp(item.created_at, `Surplus sale ${index}.created_at`), sellerCostMicroUsd: microUsd(item.seller_cost_usdc, `Surplus sale ${index}.seller_cost_usdc`), + inputTokens: optionalSafeInteger(item.input_tokens, `Surplus sale ${index}.input_tokens`), + outputTokens: optionalSafeInteger(item.output_tokens, `Surplus sale ${index}.output_tokens`), + cacheReadTokens: optionalSafeInteger( + item.cache_read_tokens, + `Surplus sale ${index}.cache_read_tokens`, + ), + effectiveInputUsdPer1m: nonNegativeNumber( + item.effective_input_per_1m, + `Surplus sale ${index}.effective_input_per_1m`, + ), + effectiveOutputUsdPer1m: nonNegativeNumber( + item.effective_output_per_1m, + `Surplus sale ${index}.effective_output_per_1m`, + ), + transactionHash: optionalString(item.tx_hash), + }; +} + +function parseDailyEarnings( + value: unknown, + index: number, +): SurplusDailyEarnings { + const item = record(value, `Surplus daily earnings ${index}`); + return { + day: string(item.day, `Surplus daily earnings ${index}.day`), + earnedUsd: nonNegativeNumber( + item.earned_usd, + `Surplus daily earnings ${index}.earned_usd`, + ), + inputTokens: nonNegativeSafeInteger( + item.input_tokens, + `Surplus daily earnings ${index}.input_tokens`, + ), + outputTokens: nonNegativeSafeInteger( + item.output_tokens, + `Surplus daily earnings ${index}.output_tokens`, + ), + requests: nonNegativeSafeInteger( + item.requests, + `Surplus daily earnings ${index}.requests`, + ), + totalTokens: nonNegativeSafeInteger( + item.total_tokens, + `Surplus daily earnings ${index}.total_tokens`, + ), + }; +} + +function parseModelEarnings( + value: unknown, + index: number, +): SurplusModelEarnings { + const item = record(value, `Surplus model earnings ${index}`); + return { + model: string(item.model, `Surplus model earnings ${index}.model`), + earnedUsd: nonNegativeNumber( + item.earned_usd, + `Surplus model earnings ${index}.earned_usd`, + ), + requests: nonNegativeSafeInteger( + item.requests, + `Surplus model earnings ${index}.requests`, + ), + totalTokens: nonNegativeSafeInteger( + item.total_tokens, + `Surplus model earnings ${index}.total_tokens`, + ), + lastSaleAt: optionalTimestamp( + item.last_sale_at, + `Surplus model earnings ${index}.last_sale_at`, + ), }; } @@ -503,6 +646,19 @@ function optionalString(value: unknown): string | null { return typeof value === "string" && value !== "" ? value : null; } +function optionalTimestamp(value: unknown, field: string): string | null { + if (value === null || value === undefined) return null; + const timestamp = typeof value === "number" + ? value + : typeof value === "string" + ? Date.parse(value) + : Number.NaN; + if (!Number.isFinite(timestamp) || timestamp < 0) { + throw new Error(`${field} must be a timestamp`); + } + return new Date(timestamp).toISOString(); +} + function boolean(value: unknown, field: string): boolean { if (typeof value !== "boolean") throw new Error(`${field} must be a boolean`); return value; @@ -560,6 +716,11 @@ function nonNegativeSafeInteger(value: unknown, field: string): number { return value; } +function optionalSafeInteger(value: unknown, field: string): number { + if (value === null || value === undefined) return 0; + return nonNegativeSafeInteger(value, field); +} + function microUsd(value: unknown, field: string): number { if (typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value)) { const parsed = Number(value); diff --git a/supplier/src/surplus/dashboard.ts b/supplier/src/surplus/dashboard.ts new file mode 100644 index 0000000..dffb0d3 --- /dev/null +++ b/supplier/src/surplus/dashboard.ts @@ -0,0 +1,259 @@ +import type { SurplusManagerConfig } from "./config.js"; +import type { + SurplusClient, + SurplusDailyEarnings, + SurplusEarnings, + SurplusOffer, + SurplusOrderBook, +} from "./client.js"; +import type { + SurplusControllerStatus, + SurplusSellerController, +} from "./controller.js"; +import type { OpenRouterKeyClient } from "./openRouterKey.js"; +import { formatUsdNanos } from "../reseller/money.js"; +import { formatMicroUsd } from "./policy.js"; + +export interface VectorMigrationSummary { + status: "retired"; + model: string; + supplierWallet: string; + advertRef: string; + retirementTransaction: string; + retiredOn: string; + historicalAp3xEarned: string; + historicalSettledJobs: number; + historicalUpstreamSpendUsd: string; +} + +export interface SurplusDashboardSnapshot { + generatedAt: string; + controller: SurplusControllerStatus; + identity: { + sellerWallet: string; + payoutAddress: string; + }; + provider: { + name: "OpenRouter"; + hardLimitUsd: string; + remainingUsd: string; + usedUsd: string; + reserveUsd: string; + estimatedDailyExposureUsd: string; + }; + surplus: { + offer: { + id: string | null; + model: string | null; + providerModel: string | null; + status: "active" | "inactive" | "missing"; + dailyCapUsd: string | null; + costMultiplier: number | null; + inputUsdPer1m: string | null; + outputUsdPer1m: string | null; + rank: number | null; + available: boolean; + healthy: boolean; + trusted: boolean; + trades24h: number; + volume24hUsd: string; + capRemainingUsd: string | null; + }; + earnings: { + totalUsd: string; + pendingUsd: string; + paidUsd: string; + requests: number; + tokens: number; + today: SurplusDailyEarnings | null; + topModel: string | null; + payoutHoldReason: string | null; + payoutHoldReleasesAt: string | null; + }; + recentSales: Array<{ + id: string; + model: string; + createdAt: string | null; + settlementStatus: string; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + revenueUsd: string; + transactionHash: string | null; + }>; + }; + vector: VectorMigrationSummary; +} + +interface SurplusDashboardOptions { + controller: Pick; + client: Pick; + allowance: Pick; + config: SurplusManagerConfig; + vector?: VectorMigrationSummary; + cacheMs?: number; + now?: () => number; +} + +const DEFAULT_VECTOR_MIGRATION: VectorMigrationSummary = { + status: "retired", + model: "deepseek/deepseek-v4-flash", + supplierWallet: "addr1v8zy05zsugau8su0lqn8q37xlryh39cww9mw9t6g7vt207c9xfc9d", + advertRef: + "29bc9e7ec9f4a26f33e419436cfe95239b5d06b75db637e8dbb4cbc478d1a445#0", + retirementTransaction: + "d7fdae603e5744c98efc20c25d73a83ebfef0308806026a370d9f81c975c11fa", + retiredOn: "2026-08-27", + historicalAp3xEarned: "1.600000", + historicalSettledJobs: 8, + historicalUpstreamSpendUsd: "0.000093", +}; + +export class SurplusDashboardService { + private readonly controller: Pick; + private readonly client: Pick< + SurplusClient, + "listAllOffers" | "getEarnings" | "getOrderBook" + >; + private readonly allowance: Pick; + private readonly config: SurplusManagerConfig; + private readonly vector: VectorMigrationSummary; + private readonly cacheMs: number; + private readonly now: () => number; + private cache: { fetchedAt: number; value: SurplusDashboardSnapshot } | null = null; + private inFlight: Promise | null = null; + + constructor(options: SurplusDashboardOptions) { + this.controller = options.controller; + this.client = options.client; + this.allowance = options.allowance; + this.config = options.config; + this.vector = options.vector ?? DEFAULT_VECTOR_MIGRATION; + this.cacheMs = options.cacheMs ?? 15_000; + this.now = options.now ?? Date.now; + } + + async getSnapshot(): Promise { + const now = this.now(); + if (this.cache && now - this.cache.fetchedAt < this.cacheMs) { + return this.cache.value; + } + if (this.inFlight) return this.inFlight; + const load = this.loadSnapshot(); + this.inFlight = load; + try { + const value = await load; + this.cache = { fetchedAt: this.now(), value }; + return value; + } finally { + this.inFlight = null; + } + } + + private async loadSnapshot(): Promise { + const controller = this.controller.snapshot(); + const [offers, earnings, allowance, orderBook] = await Promise.all([ + this.client.listAllOffers(), + this.client.getEarnings(), + this.allowance.readAllowance(), + controller.model ? this.client.getOrderBook(controller.model) : null, + ]); + const offer = findOffer(offers, controller.offerId); + const marketOffer = findMarketOffer(orderBook, controller.offerId); + const usedUsdNanos = allowance.limitUsdNanos - allowance.remainingUsdNanos; + const estimatedDailyExposure = + Number(this.config.perOfferCapUsd) * 10_000 / this.config.recoveryBps; + const today = earnings.daily.find((day) => + day.day === new Date(this.now()).toISOString().slice(0, 10) + ) ?? null; + + return { + generatedAt: new Date(this.now()).toISOString(), + controller, + identity: { + sellerWallet: this.config.sellerWallet, + payoutAddress: this.config.payoutAddress, + }, + provider: { + name: "OpenRouter", + hardLimitUsd: formatUsdNanos(allowance.limitUsdNanos, 6), + remainingUsd: formatUsdNanos(allowance.remainingUsdNanos, 6), + usedUsd: formatUsdNanos(usedUsdNanos, 6), + reserveUsd: this.config.reserveUsd, + estimatedDailyExposureUsd: estimatedDailyExposure.toFixed(6), + }, + surplus: { + offer: { + id: offer?.id ?? controller.offerId, + model: offer?.model ?? controller.model, + providerModel: controller.providerModelId, + status: offer?.status ?? "missing", + dailyCapUsd: offer?.capDailyUsd === null || offer === undefined + ? null + : offer.capDailyUsd.toFixed(6), + costMultiplier: offer?.costMultiplierPpm === null || offer === undefined + ? null + : offer.costMultiplierPpm / 1_000_000, + inputUsdPer1m: controller.inputUsdPer1m, + outputUsdPer1m: controller.outputUsdPer1m, + rank: marketOffer?.rank ?? null, + available: marketOffer?.available ?? false, + healthy: marketOffer?.healthy ?? false, + trusted: marketOffer?.trusted ?? false, + trades24h: marketOffer?.trades24h ?? 0, + volume24hUsd: formatMicroUsd(marketOffer?.volume24hMicroUsd ?? 0), + capRemainingUsd: marketOffer?.capRemainingMicroUsd === null || + marketOffer === undefined + ? null + : formatMicroUsd(marketOffer.capRemainingMicroUsd), + }, + earnings: earningsSummary(earnings, today), + recentSales: earnings.recentSales.slice(0, 25).map((sale) => ({ + id: sale.id, + model: sale.model, + createdAt: sale.createdAt, + settlementStatus: sale.settlementStatus, + inputTokens: sale.inputTokens, + outputTokens: sale.outputTokens, + cacheReadTokens: sale.cacheReadTokens, + revenueUsd: formatMicroUsd(sale.sellerCostMicroUsd), + transactionHash: sale.transactionHash, + })), + }, + vector: this.vector, + }; + } +} + +function findOffer( + offers: SurplusOffer[], + offerId: string | null, +): SurplusOffer | undefined { + return offerId ? offers.find((offer) => offer.id === offerId) : undefined; +} + +function findMarketOffer( + orderBook: SurplusOrderBook | null, + offerId: string | null, +) { + return offerId + ? orderBook?.offers.find((offer) => offer.id === offerId) + : undefined; +} + +function earningsSummary( + earnings: SurplusEarnings, + today: SurplusDailyEarnings | null, +): SurplusDashboardSnapshot["surplus"]["earnings"] { + return { + totalUsd: formatMicroUsd(earnings.totalEarnedMicroUsd), + pendingUsd: formatMicroUsd(earnings.pendingMicroUsd), + paidUsd: formatMicroUsd(earnings.paidMicroUsd), + requests: earnings.requestCount, + tokens: earnings.tokenCount, + today, + topModel: earnings.topModel, + payoutHoldReason: earnings.payoutHoldReason, + payoutHoldReleasesAt: earnings.payoutHoldReleasesAt, + }; +} diff --git a/supplier/src/surplus/dashboardPage.ts b/supplier/src/surplus/dashboardPage.ts new file mode 100644 index 0000000..42abf46 --- /dev/null +++ b/supplier/src/surplus/dashboardPage.ts @@ -0,0 +1,62 @@ +export function renderSurplusDashboardPage(): string { + return ` + + + + + +Inference Resale Control Plane + + + +
+
Autonomous inference resale

One capacity pool.
Two settlement markets.

Live OpenRouter capacity sold through Surplus on Base, with the completed Vector AP3X proof preserved as the migration record.

Loading controller
Waiting for current state
+
+
Upstream capacityOpenRouterAllowance loadingprovider model --
+
Autonomous managerSeller agentDiscovers · prices · caps · reconcilesseller --
+
Current settlementSurplus / Base USDCOffer loadingoffer --
+
+
+
Current market

Surplus Intelligence

Competitive model routing with USDC settlement on Base.

Loading
+
Lifetime revenue----
Requests sold---- tokens
Market position----
Daily seller cap---- remaining
+
+
Migration record

Vector marketplace

AP3X escrow route retired after the cross-market proof.

Retired
+
AP3X earned--historical
Settled jobs--Vector escrows
Upstream spend--proof route
Retired on--advert closed
+
+
+
Economics

Capacity and price controls

The seller cap is discounted revenue. The agent converts it to estimated upstream exposure before it opens the route.

Bounded
+
OpenRouter remaining---- used
Protected reserve--fail-closed threshold
Daily upstream estimate--from recovery floor
Current multiplier----
+
Surplus activity

Recent sales

Loading settlement records

+
TimeStatusModelInputOutputCache readRevenueBase settlement
Loading sales
+
Seller identity loadingVector proves the agent-owned route. Surplus is the current USDC market.
+
+ + +`; +} diff --git a/tests/unit/surplus-client.test.ts b/tests/unit/surplus-client.test.ts index 18a29f8..a45b8f5 100644 --- a/tests/unit/surplus-client.test.ts +++ b/tests/unit/surplus-client.test.ts @@ -201,6 +201,9 @@ describe("SurplusClient", () => { healthy: true, trusted: true, trades_24h: 7, + rank: 1, + cap_remaining: null, + volume_24h: 0, }], }); }); @@ -222,6 +225,79 @@ describe("SurplusClient", () => { expect(String(fetchFn.mock.calls[1][0])).toContain("alpha%3Amodel"); }); + it("normalizes live earnings, payout hold, and settlement records", async () => { + const client = new SurplusClient({ + apiBaseUrl: "https://api.surplusintelligence.ai", + sellerApiKey: SELLER_KEY, + timeoutMs: 1_000, + fetchFn: async () => Response.json({ + total_earned_usdc: "186810", + pending_usdc: "176817", + paid_usdc: "9993", + daily: [{ + day: "2026-08-27", + earned_usd: 0.18681, + input_tokens: 100, + output_tokens: 20, + requests: 3, + total_tokens: 120, + }], + by_model: [{ + model: "alpha", + earned_usd: 0.18681, + requests: 3, + total_tokens: 120, + last_sale_at: 1787831146968, + }], + share: { + requests: 3, + tokens: 120, + top_model: "alpha", + }, + recent_sales: [{ + id: "sale-1", + model: "alpha", + offer_id: "offer-1", + settlement_status: "confirmed", + created_at: 1787831146968, + seller_cost_usdc: "83", + input_tokens: 10, + output_tokens: 2, + cache_read_tokens: 4, + effective_input_per_1m: 0.004, + effective_output_per_1m: 0.008, + tx_hash: "0x" + "a".repeat(64), + }], + payout_hold: { + reason: "new_seller", + releases_at: "2026-08-29T11:24:42.552Z", + }, + }), + }); + + const earnings = await client.getEarnings(); + + expect(earnings).toMatchObject({ + totalEarnedMicroUsd: 186_810, + pendingMicroUsd: 176_817, + paidMicroUsd: 9_993, + requestCount: 3, + tokenCount: 120, + topModel: "alpha", + payoutHoldReason: "new_seller", + }); + expect(earnings.daily[0]).toMatchObject({ requests: 3, totalTokens: 120 }); + expect(earnings.recentSales[0]).toMatchObject({ + id: "sale-1", + settlementStatus: "confirmed", + cacheReadTokens: 4, + sellerCostMicroUsd: 83, + }); + expect(earnings.recentSales[0].createdAt).toBe( + new Date(1787831146968).toISOString(), + ); + }); + it("sends one absolute cost-multiplier PATCH with the cap", async () => { let request: RequestInit | undefined; const client = new SurplusClient({ diff --git a/tests/unit/surplus-controller.test.ts b/tests/unit/surplus-controller.test.ts index 52740e9..2f345f1 100644 --- a/tests/unit/surplus-controller.test.ts +++ b/tests/unit/surplus-controller.test.ts @@ -114,6 +114,9 @@ class FakeSurplusClient implements SurplusControllerClient { healthy: true, trusted: true, trades24h: 0, + rank: 1, + capRemainingMicroUsd: null, + volume24hMicroUsd: 0, }]; if (this.offer) { offers.push({ @@ -126,6 +129,9 @@ class FakeSurplusClient implements SurplusControllerClient { healthy: true, trusted: true, trades24h: this.trades24h, + rank: 2, + capRemainingMicroUsd: 50_000, + volume24hMicroUsd: 0, }); } return { model, offers }; @@ -187,12 +193,26 @@ class FakeSurplusClient implements SurplusControllerClient { totalEarnedMicroUsd: this.settled ? 100 : 0, pendingMicroUsd: 0, paidMicroUsd: this.settled ? 100 : 0, + daily: [], + byModel: [], + requestCount: this.settled ? 1 : 0, + tokenCount: this.settled ? 2 : 0, + topModel: this.settled ? "beta-model" : null, + payoutHoldReason: null, + payoutHoldReleasesAt: null, recentSales: this.settled ? [{ + id: "sale-1", model: "beta-model", offerId: "managed-offer", settlementStatus: "confirmed", createdAt: "2026-08-27T10:00:01.000Z", sellerCostMicroUsd: 100, + inputTokens: 1, + outputTokens: 1, + cacheReadTokens: 0, + effectiveInputUsdPer1m: 0.003972, + effectiveOutputUsdPer1m: 0.007944, + transactionHash: "0x" + "a".repeat(64), }] : [], }; } diff --git a/tests/unit/surplus-dashboard.test.ts b/tests/unit/surplus-dashboard.test.ts new file mode 100644 index 0000000..3979983 --- /dev/null +++ b/tests/unit/surplus-dashboard.test.ts @@ -0,0 +1,259 @@ +import { describe, expect, it, vi } from "vitest"; +import request from "supertest"; +import type { SurplusManagerConfig } from "../../supplier/src/surplus/config.js"; +import { + SurplusDashboardService, + type SurplusDashboardSnapshot, +} from "../../supplier/src/surplus/dashboard.js"; +import { renderSurplusDashboardPage } from "../../supplier/src/surplus/dashboardPage.js"; +import { createSurplusServer } from "../../supplier/src/cli/surplus-seller.js"; +import type { SurplusControllerStatus } from "../../supplier/src/surplus/controller.js"; + +const STATUS: SurplusControllerStatus = { + ok: true, + phase: "active", + live: true, + model: "deepseek-v4-flash", + providerModelId: "deepseek/deepseek-v4-flash", + offerId: "offer-1", + inputUsdPer1m: "0.003972", + outputUsdPer1m: "0.007944", + competitorId: "external", + remainingAllowanceUsd: "16.313443", + reserveThresholdUsd: "1.00", + totalEarnedUsd: "0.186810", + lastCycleAt: "2026-08-28T12:00:00.000Z", + lastSuccessAt: "2026-08-28T12:00:00.000Z", + lastError: null, +}; + +function config(): SurplusManagerConfig { + return { + live: true, + apiBaseUrl: "https://api.surplusintelligence.ai", + sellerApiKey: "si_seller_test_secret", + providerApiKey: "sk-or-v1-test-secret", + providerBaseUrl: "https://openrouter.ai/api/v1", + capacityBaseUrl: "https://openrouter.ai/api", + perOfferCapUsd: "0.05", + aggregateCapUsd: "0.05", + sellerWallet: "0x1111111111111111111111111111111111111111", + payoutAddress: "0x1111111111111111111111111111111111111111", + maxCandidateOrderBooks: 10, + reserveUsd: "1.00", + maxProviderLimitUsd: "20.00", + recoveryBps: 490, + undercutBps: 10, + pollIntervalMs: 300_000, + requestTimeoutMs: 120_000, + stopAfterTrades: 0, + settledStatuses: ["confirmed"], + statePath: "/tmp/surplus-dashboard-test.json", + port: 8080, + }; +} + +function snapshot(): SurplusDashboardSnapshot { + return { + generatedAt: "2026-08-28T12:00:00.000Z", + controller: STATUS, + identity: { + sellerWallet: "0x1111111111111111111111111111111111111111", + payoutAddress: "0x1111111111111111111111111111111111111111", + }, + provider: { + name: "OpenRouter", + hardLimitUsd: "20.000000", + remainingUsd: "16.313443", + usedUsd: "3.686557", + reserveUsd: "1.00", + estimatedDailyExposureUsd: "1.020408", + }, + surplus: { + offer: { + id: "offer-1", + model: "deepseek-v4-flash", + providerModel: "deepseek/deepseek-v4-flash", + status: "active", + dailyCapUsd: "0.050000", + costMultiplier: 0.04995, + inputUsdPer1m: "0.003972", + outputUsdPer1m: "0.007944", + rank: 10, + available: true, + healthy: true, + trusted: true, + trades24h: 429, + volume24hUsd: "0.064030", + capRemainingUsd: "0.050000", + }, + earnings: { + totalUsd: "0.186810", + pendingUsd: "0.176817", + paidUsd: "0.009993", + requests: 1_093, + tokens: 104_698_905, + today: null, + topModel: "deepseek-v4-flash", + payoutHoldReason: "new_seller", + payoutHoldReleasesAt: "2026-08-29T11:24:42.552Z", + }, + recentSales: [], + }, + vector: { + status: "retired", + model: "deepseek/deepseek-v4-flash", + supplierWallet: "addr1test", + advertRef: `${"a".repeat(64)}#0`, + retirementTransaction: "b".repeat(64), + retiredOn: "2026-08-27", + historicalAp3xEarned: "1.600000", + historicalSettledJobs: 8, + historicalUpstreamSpendUsd: "0.000093", + }, + }; +} + +describe("SurplusDashboardService", () => { + it("combines safe live metrics with the Vector migration record", async () => { + const listAllOffers = vi.fn(async () => [{ + id: "offer-1", + model: "deepseek-v4-flash", + sellerBaseUrl: "https://openrouter.ai/api/v1", + status: "active" as const, + capDailyUsd: 0.05, + costMultiplierPpm: 49_950, + inputMicroUsdPer1m: 3_972, + outputMicroUsdPer1m: 7_944, + }]); + const getEarnings = vi.fn(async () => ({ + totalEarnedMicroUsd: 186_810, + pendingMicroUsd: 176_817, + paidMicroUsd: 9_993, + daily: [{ + day: "2026-08-28", + earnedUsd: 0.01, + inputTokens: 10, + outputTokens: 2, + requests: 1, + totalTokens: 12, + }], + byModel: [], + requestCount: 1_093, + tokenCount: 104_698_905, + topModel: "deepseek-v4-flash", + payoutHoldReason: "new_seller", + payoutHoldReleasesAt: "2026-08-29T11:24:42.552Z", + recentSales: [{ + id: "sale-1", + model: "deepseek-v4-flash", + offerId: "offer-1", + settlementStatus: "confirmed", + createdAt: "2026-08-28T11:00:00.000Z", + sellerCostMicroUsd: 83, + inputTokens: 10, + outputTokens: 2, + cacheReadTokens: 3, + effectiveInputUsdPer1m: 0.003972, + effectiveOutputUsdPer1m: 0.007944, + transactionHash: "0x" + "c".repeat(64), + }], + })); + const getOrderBook = vi.fn(async () => ({ + model: "deepseek-v4-flash", + offers: [{ + id: "offer-1", + seller: "0x1111111111111111111111111111111111111111", + sellerBaseUrl: "https://openrouter.ai/api/v1", + inputMicroUsdPer1m: 3_972, + outputMicroUsdPer1m: 7_944, + available: true, + healthy: true, + trusted: true, + trades24h: 429, + rank: 10, + capRemainingMicroUsd: 50_000, + volume24hMicroUsd: 64_030, + }], + })); + const service = new SurplusDashboardService({ + controller: { snapshot: () => STATUS }, + client: { listAllOffers, getEarnings, getOrderBook }, + allowance: { + readAllowance: async () => ({ + limitUsdNanos: 20_000_000_000n, + remainingUsdNanos: 16_313_443_131n, + reset: null, + checkedAtMs: Date.parse("2026-08-28T12:00:00.000Z"), + }), + }, + config: config(), + now: () => Date.parse("2026-08-28T12:00:00.000Z"), + }); + + const first = await service.getSnapshot(); + const second = await service.getSnapshot(); + + expect(first.surplus.offer).toMatchObject({ + id: "offer-1", + rank: 10, + available: true, + dailyCapUsd: "0.050000", + }); + expect(first.surplus.earnings).toMatchObject({ + totalUsd: "0.186810", + requests: 1_093, + topModel: "deepseek-v4-flash", + }); + expect(first.provider).toMatchObject({ + usedUsd: "3.686556", + estimatedDailyExposureUsd: "1.020408", + }); + expect(first.vector).toMatchObject({ status: "retired", historicalSettledJobs: 8 }); + expect(first.surplus.recentSales[0]).toMatchObject({ + settlementStatus: "confirmed", + revenueUsd: "0.000083", + }); + expect(JSON.stringify(first)).not.toContain("si_seller_test_secret"); + expect(JSON.stringify(first)).not.toContain("sk-or-v1-test-secret"); + expect(second).toBe(first); + expect(listAllOffers).toHaveBeenCalledTimes(1); + expect(getEarnings).toHaveBeenCalledTimes(1); + expect(getOrderBook).toHaveBeenCalledTimes(1); + }); +}); + +describe("unified dashboard HTTP surface", () => { + it("serves the dashboard, cached API data, and security headers", async () => { + const data = snapshot(); + const server = createSurplusServer( + { snapshot: () => STATUS, healthy: () => true }, + { getSnapshot: async () => data }, + ); + + const page = await request(server).get("/reseller"); + expect(page.status).toBe(200); + expect(page.text).toContain("One capacity pool."); + expect(page.text).toContain("Vector marketplace"); + expect(page.headers["content-security-policy"]).toContain("frame-ancestors 'none'"); + + const api = await request(server).get("/dashboard/api"); + expect(api.status).toBe(200); + expect(api.body).toEqual(data); + expect(JSON.stringify(api.body)).not.toContain("test-secret"); + + const root = await request(server).get("/"); + expect(root.status).toBe(302); + expect(root.headers.location).toBe("/reseller"); + server.close(); + }); + + it("renders static HTML without embedding credentials", () => { + const html = renderSurplusDashboardPage(); + expect(html).toContain("/dashboard/api"); + expect(html).toContain("Surplus Intelligence"); + expect(html).toContain("Vector marketplace"); + expect(html).not.toContain("si_seller_"); + expect(html).not.toContain("sk-or-v1-"); + }); +});