feat: add autonomous Surplus seller
This commit is contained in:
@@ -132,3 +132,28 @@ ADVERT_DETAIL_HASH=
|
||||
# MAX_CHAT_SESSIONS=1 # concurrent session slots (default 1)
|
||||
# CHAT_SETTLE_MODE=full # "full" (Claim/Submit) or "ticket" (no supplier
|
||||
# # chain ops; buyer reclaims the Open escrow)
|
||||
|
||||
# ─── Autonomous Surplus seller (surplus:start) ────────────────────────────
|
||||
# This service uses a seller-scoped Surplus key. It does not need a wallet
|
||||
# private key. Keep both API keys in the host-only surplus-seller.env file.
|
||||
SURPLUS_LIVE=
|
||||
SURPLUS_API_URL=https://api.surplusintelligence.ai
|
||||
SURPLUS_SELLER_API_KEY=si_seller_REPLACE_ME
|
||||
SURPLUS_OPENROUTER_API_KEY=sk-or-v1-REPLACE_ME
|
||||
SURPLUS_PROVIDER_BASE_URL=https://openrouter.ai/api/v1
|
||||
OPENROUTER_CAPACITY_BASE_URL=https://openrouter.ai/api
|
||||
SURPLUS_SELLER_WALLET=0x0000000000000000000000000000000000000000
|
||||
SURPLUS_PAYOUT_ADDRESS=0x0000000000000000000000000000000000000000
|
||||
# The controller discovers all fixed-price text models and selects one by
|
||||
# current demand. It keeps only one active proof offer.
|
||||
SURPLUS_PER_OFFER_CAP_USD=1.00
|
||||
SURPLUS_AGGREGATE_CAP_USD=1.00
|
||||
SURPLUS_OPENROUTER_RESERVE_USD=1.00
|
||||
SURPLUS_OPENROUTER_MAX_LIMIT_USD=20.00
|
||||
SURPLUS_MIN_RECOVERY_BPS=490
|
||||
SURPLUS_UNDERCUT_BPS=10
|
||||
SURPLUS_MAX_CANDIDATE_ORDER_BOOKS=10
|
||||
SURPLUS_REPRICE_INTERVAL_MS=300000
|
||||
SURPLUS_REQUEST_TIMEOUT_MS=120000
|
||||
SURPLUS_STOP_AFTER_SETTLED_REQUESTS=1
|
||||
SURPLUS_STATE_PATH=/var/lib/surplus-manager/state.json
|
||||
|
||||
@@ -70,6 +70,9 @@ COPY --from=builder /repo /repo
|
||||
|
||||
# Drop privileges. The official node image ships a non-root `node` user.
|
||||
RUN chown -R node:node /repo
|
||||
RUN mkdir -p /var/lib/surplus-manager \
|
||||
&& chown node:node /var/lib/surplus-manager \
|
||||
&& chmod 700 /var/lib/surplus-manager
|
||||
USER node
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"tx:retire-advert": "tsx src/cli/retire-advert.ts",
|
||||
"tx:publish-reference-scripts": "tsx src/cli/publish-reference-scripts.ts",
|
||||
"tx:consolidate-wallet": "tsx src/cli/consolidate-wallet.ts",
|
||||
"reseller:setup": "tsx src/cli/setup-reseller.ts"
|
||||
"reseller:setup": "tsx src/cli/setup-reseller.ts",
|
||||
"surplus:start": "tsx src/cli/surplus-seller.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@marketplace/shared": "workspace:*",
|
||||
|
||||
92
supplier/src/cli/surplus-seller.ts
Normal file
92
supplier/src/cli/surplus-seller.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { createServer, type Server } from "node:http";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { loadSurplusManagerConfig } from "../surplus/config.js";
|
||||
import { SurplusClient } from "../surplus/client.js";
|
||||
import { SurplusSellerController } from "../surplus/controller.js";
|
||||
import { OpenRouterKeyClient } from "../surplus/openRouterKey.js";
|
||||
import { FileSurplusStateStore } from "../surplus/state.js";
|
||||
|
||||
export async function runSurplusSeller(
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<{ server: Server; controller: SurplusSellerController }> {
|
||||
const config = loadSurplusManagerConfig(env);
|
||||
const client = new SurplusClient({
|
||||
apiBaseUrl: config.apiBaseUrl,
|
||||
sellerApiKey: config.sellerApiKey,
|
||||
timeoutMs: config.requestTimeoutMs,
|
||||
});
|
||||
const allowance = new OpenRouterKeyClient({
|
||||
baseUrl: config.capacityBaseUrl,
|
||||
apiKey: config.providerApiKey,
|
||||
timeoutMs: config.requestTimeoutMs,
|
||||
});
|
||||
const stateStore = new FileSurplusStateStore(config.statePath);
|
||||
const controller = new SurplusSellerController({
|
||||
config,
|
||||
client,
|
||||
allowance,
|
||||
stateStore,
|
||||
log: (message) => console.log(message),
|
||||
});
|
||||
const server = createStatusServer(controller);
|
||||
await new Promise<void>((resolveListen, rejectListen) => {
|
||||
server.once("error", rejectListen);
|
||||
server.listen(config.port, "0.0.0.0", () => {
|
||||
server.off("error", rejectListen);
|
||||
resolveListen();
|
||||
});
|
||||
});
|
||||
await controller.start();
|
||||
return { server, controller };
|
||||
}
|
||||
|
||||
function createStatusServer(controller: SurplusSellerController): 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") {
|
||||
response.statusCode = 200;
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.end(JSON.stringify(status));
|
||||
return;
|
||||
}
|
||||
response.statusCode = 404;
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.end(JSON.stringify({ error: "not_found" }));
|
||||
});
|
||||
}
|
||||
|
||||
export async function main(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
): Promise<void> {
|
||||
const { server, controller } = await runSurplusSeller(env);
|
||||
console.log("Surplus seller controller listening");
|
||||
let shuttingDown = false;
|
||||
const shutdown = async (signal: string): Promise<void> => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
console.log(`Surplus seller controller shutting down (${signal})`);
|
||||
await controller.stop();
|
||||
await new Promise<void>((resolveClose, rejectClose) => {
|
||||
server.close((error) => error ? rejectClose(error) : resolveClose());
|
||||
});
|
||||
};
|
||||
process.on("SIGINT", () => void shutdown("SIGINT"));
|
||||
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
||||
}
|
||||
|
||||
const invoked = fileURLToPath(import.meta.url) === resolve(process.argv[1] ?? "");
|
||||
if (invoked) {
|
||||
main().catch((error) => {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Surplus seller controller failed: ${detail}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
508
supplier/src/surplus/client.ts
Normal file
508
supplier/src/surplus/client.ts
Normal file
@@ -0,0 +1,508 @@
|
||||
export interface SurplusOffer {
|
||||
id: string;
|
||||
model: string;
|
||||
sellerBaseUrl: string;
|
||||
status: "active" | "inactive";
|
||||
capDailyUsd: number | null;
|
||||
inputMicroUsdPer1m: number | null;
|
||||
outputMicroUsdPer1m: number | null;
|
||||
}
|
||||
|
||||
export interface SurplusDiscoveredModel {
|
||||
model: string;
|
||||
supported: boolean;
|
||||
inputUsdPer1m: number;
|
||||
outputUsdPer1m: number;
|
||||
priceUnit: string;
|
||||
priceVariable: boolean;
|
||||
providerModelId: string;
|
||||
modelType: string;
|
||||
availabilityStatus: string;
|
||||
}
|
||||
|
||||
export interface SurplusMarketSummary {
|
||||
model: string;
|
||||
requests24h: number;
|
||||
volume24h: number;
|
||||
bestInputMicroUsdPer1m: number;
|
||||
bestOutputMicroUsdPer1m: number;
|
||||
healthySellerCount: number;
|
||||
}
|
||||
|
||||
export interface SurplusOrderBookOffer {
|
||||
id: string;
|
||||
seller: string;
|
||||
sellerBaseUrl: string;
|
||||
inputMicroUsdPer1m: number;
|
||||
outputMicroUsdPer1m: number;
|
||||
available: boolean;
|
||||
healthy: boolean;
|
||||
trusted: boolean;
|
||||
trades24h: number;
|
||||
}
|
||||
|
||||
export interface SurplusOrderBook {
|
||||
model: string;
|
||||
offers: SurplusOrderBookOffer[];
|
||||
}
|
||||
|
||||
export interface SurplusOfferWrite {
|
||||
model: string;
|
||||
apiKey: string;
|
||||
sellerBaseUrl: string;
|
||||
inputUsdPer1m: number;
|
||||
outputUsdPer1m: number;
|
||||
dailyCapUsd: number;
|
||||
payoutAddress: string;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface SurplusOfferPatch {
|
||||
inputUsdPer1m: number;
|
||||
outputUsdPer1m: number;
|
||||
dailyCapUsd: number;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface SurplusSale {
|
||||
model: string;
|
||||
offerId: string | null;
|
||||
settlementStatus: string;
|
||||
createdAt: string | null;
|
||||
sellerCostMicroUsd: number;
|
||||
}
|
||||
|
||||
export interface SurplusEarnings {
|
||||
totalEarnedMicroUsd: number;
|
||||
pendingMicroUsd: number;
|
||||
paidMicroUsd: number;
|
||||
recentSales: SurplusSale[];
|
||||
}
|
||||
|
||||
interface SurplusClientOptions {
|
||||
apiBaseUrl: string;
|
||||
sellerApiKey: string;
|
||||
timeoutMs: number;
|
||||
fetchFn?: typeof globalThis.fetch;
|
||||
}
|
||||
|
||||
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
export class SurplusHttpError extends Error {
|
||||
readonly status: number | null;
|
||||
readonly retryAfter: string | null;
|
||||
|
||||
constructor(message: string, status: number | null, retryAfter: string | null = null) {
|
||||
super(message);
|
||||
this.name = "SurplusHttpError";
|
||||
this.status = status;
|
||||
this.retryAfter = retryAfter;
|
||||
}
|
||||
|
||||
get ambiguousMutation(): boolean {
|
||||
return this.status === null || this.status >= 500;
|
||||
}
|
||||
}
|
||||
|
||||
export class SurplusClient {
|
||||
private readonly apiBaseUrl: string;
|
||||
private readonly sellerApiKey: string;
|
||||
private readonly timeoutMs: number;
|
||||
private readonly fetchFn: typeof globalThis.fetch;
|
||||
|
||||
constructor(options: SurplusClientOptions) {
|
||||
this.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
|
||||
this.sellerApiKey = options.sellerApiKey;
|
||||
this.timeoutMs = options.timeoutMs;
|
||||
this.fetchFn = options.fetchFn ?? globalThis.fetch;
|
||||
}
|
||||
|
||||
async listAllOffers(): Promise<SurplusOffer[]> {
|
||||
const items: SurplusOffer[] = [];
|
||||
const seenTokens = new Set<string>();
|
||||
let nextToken: string | null = null;
|
||||
for (;;) {
|
||||
const params = new URLSearchParams();
|
||||
if (nextToken !== null) params.set("next_token", nextToken);
|
||||
const query = params.toString();
|
||||
const body = await this.requestJson(`/v1/seller/offers${query ? `?${query}` : ""}`);
|
||||
const root = record(body, "Surplus offers response");
|
||||
if (!Array.isArray(root.items)) {
|
||||
throw new Error("Surplus offers response.items must be an array");
|
||||
}
|
||||
items.push(...root.items.map((item, index) => parseOffer(item, items.length + index)));
|
||||
const rawNext = root.next_token;
|
||||
if (rawNext === undefined || rawNext === null || rawNext === "") break;
|
||||
if (typeof rawNext !== "string") {
|
||||
throw new Error("Surplus offers response.next_token must be a string or null");
|
||||
}
|
||||
if (seenTokens.has(rawNext)) {
|
||||
throw new Error("Surplus offers pagination repeated a next_token");
|
||||
}
|
||||
seenTokens.add(rawNext);
|
||||
nextToken = rawNext;
|
||||
if (seenTokens.size > 10_000) {
|
||||
throw new Error("Surplus offers pagination exceeded 10000 pages");
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async discoverModels(apiKey: string, baseUrl: string): Promise<SurplusDiscoveredModel[]> {
|
||||
const response = await this.requestText("/v1/seller/discover", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ api_key: apiKey, base_url: baseUrl }),
|
||||
});
|
||||
if (response.text.trim() === "") {
|
||||
throw new Error("Surplus discovery returned an empty body");
|
||||
}
|
||||
return response.text
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.trim() !== "")
|
||||
.map((line, index) => {
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(line);
|
||||
} catch {
|
||||
throw new Error(`Surplus discovery line ${index + 1} was not complete JSON`);
|
||||
}
|
||||
return parseDiscoveredModel(value, index);
|
||||
});
|
||||
}
|
||||
|
||||
async getMarkets(): Promise<SurplusMarketSummary[]> {
|
||||
const body = await this.requestJson("/api/markets", {}, false);
|
||||
const root = record(body, "Surplus markets response");
|
||||
if (!Array.isArray(root.markets)) {
|
||||
throw new Error("Surplus markets response.markets must be an array");
|
||||
}
|
||||
return root.markets.map((item, index) => parseMarketSummary(item, index));
|
||||
}
|
||||
|
||||
async getOrderBook(model: string): Promise<SurplusOrderBook> {
|
||||
const body = await this.requestJson(
|
||||
`/api/markets/${encodeURIComponent(model)}`,
|
||||
{},
|
||||
false,
|
||||
);
|
||||
const root = record(body, "Surplus order book response");
|
||||
const returnedModel = string(root.model, "Surplus order book model");
|
||||
if (returnedModel !== model) {
|
||||
throw new Error(`Surplus order book returned model ${returnedModel} for ${model}`);
|
||||
}
|
||||
if (!Array.isArray(root.offers)) {
|
||||
throw new Error("Surplus order book response.offers must be an array");
|
||||
}
|
||||
const ids = new Set<string>();
|
||||
const offers = root.offers.map((item, index) => {
|
||||
const offer = parseOrderBookOffer(item, index);
|
||||
if (ids.has(offer.id)) throw new Error(`Surplus order book repeated offer ${offer.id}`);
|
||||
ids.add(offer.id);
|
||||
return offer;
|
||||
});
|
||||
return { model, offers };
|
||||
}
|
||||
|
||||
async testConnection(apiKey: string, baseUrl: string, model: string): Promise<number> {
|
||||
const body = await this.requestJson("/v1/seller/test-connection", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ api_key: apiKey, base_url: baseUrl, model }),
|
||||
});
|
||||
const root = record(body, "Surplus connection response");
|
||||
if (root.ok !== true) throw new Error("Surplus provider connection test failed");
|
||||
return nonNegativeNumber(root.latency_ms, "Surplus connection latency_ms");
|
||||
}
|
||||
|
||||
async createOffer(input: SurplusOfferWrite): Promise<string> {
|
||||
const body = await this.requestJson("/v1/seller/offers", {
|
||||
method: "POST",
|
||||
headers: mutationHeaders(input.idempotencyKey),
|
||||
body: JSON.stringify({
|
||||
model: input.model,
|
||||
api_key: input.apiKey,
|
||||
seller_base_url: input.sellerBaseUrl,
|
||||
pricing_mode: "per_token",
|
||||
price_input_per_1m: input.inputUsdPer1m,
|
||||
price_output_per_1m: input.outputUsdPer1m,
|
||||
cap_daily_usd: input.dailyCapUsd,
|
||||
payout_address: input.payoutAddress,
|
||||
}),
|
||||
});
|
||||
const root = record(body, "Surplus create offer response");
|
||||
return offerId(root, "Surplus create offer response");
|
||||
}
|
||||
|
||||
async updateOffer(id: string, patch: SurplusOfferPatch): Promise<void> {
|
||||
await this.requestText(`/v1/seller/offers/${encodeURIComponent(id)}`, {
|
||||
method: "PATCH",
|
||||
headers: mutationHeaders(patch.idempotencyKey),
|
||||
body: JSON.stringify({
|
||||
pricing_mode: "per_token",
|
||||
price_input_per_1m: patch.inputUsdPer1m,
|
||||
price_output_per_1m: patch.outputUsdPer1m,
|
||||
cap_daily_usd: patch.dailyCapUsd,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async pauseOffer(id: string, idempotencyKey: string): Promise<void> {
|
||||
await this.requestText(`/v1/seller/offers/${encodeURIComponent(id)}`, {
|
||||
method: "PATCH",
|
||||
headers: mutationHeaders(idempotencyKey),
|
||||
body: JSON.stringify({ status: "paused" }),
|
||||
});
|
||||
}
|
||||
|
||||
async resumeOffer(id: string, idempotencyKey: string): Promise<void> {
|
||||
await this.requestText(`/v1/seller/offers/${encodeURIComponent(id)}`, {
|
||||
method: "PATCH",
|
||||
headers: mutationHeaders(idempotencyKey),
|
||||
body: JSON.stringify({ status: "active" }),
|
||||
});
|
||||
}
|
||||
|
||||
async getEarnings(): Promise<SurplusEarnings> {
|
||||
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)) {
|
||||
throw new Error("Surplus earnings response.recent_sales must be an array");
|
||||
}
|
||||
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)),
|
||||
};
|
||||
}
|
||||
|
||||
private async requestJson(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
authenticated = true,
|
||||
): Promise<unknown> {
|
||||
const response = await this.requestText(path, init, authenticated);
|
||||
if (response.text === "") return {};
|
||||
try {
|
||||
return JSON.parse(response.text);
|
||||
} catch {
|
||||
throw new Error(`Surplus ${path} response was not JSON`);
|
||||
}
|
||||
}
|
||||
|
||||
private async requestText(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
authenticated = true,
|
||||
): Promise<{ status: number; text: string }> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
const headers = new Headers(init.headers);
|
||||
if (authenticated) headers.set("authorization", `Bearer ${this.sellerApiKey}`);
|
||||
try {
|
||||
const response = await this.fetchFn(`${this.apiBaseUrl}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
redirect: "error",
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await readBoundedText(response, controller);
|
||||
if (!response.ok) {
|
||||
throw new SurplusHttpError(
|
||||
`Surplus ${path} returned HTTP ${response.status}${
|
||||
text ? `: ${sanitizedErrorSnippet(text)}` : ""
|
||||
}`,
|
||||
response.status,
|
||||
response.headers.get("retry-after"),
|
||||
);
|
||||
}
|
||||
return { status: response.status, text };
|
||||
} catch (error) {
|
||||
if (error instanceof SurplusHttpError) throw error;
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
throw new SurplusHttpError(`Surplus ${path} request failed: ${detail}`, null);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readBoundedText(
|
||||
response: Response,
|
||||
controller: AbortController,
|
||||
): Promise<string> {
|
||||
if (!response.body) return "";
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let bytes = 0;
|
||||
let text = "";
|
||||
try {
|
||||
for (;;) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
bytes += chunk.value.byteLength;
|
||||
if (bytes > MAX_RESPONSE_BYTES) {
|
||||
controller.abort();
|
||||
throw new Error(`response exceeded ${MAX_RESPONSE_BYTES} bytes`);
|
||||
}
|
||||
text += decoder.decode(chunk.value, { stream: true });
|
||||
}
|
||||
return text + decoder.decode();
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
function mutationHeaders(idempotencyKey: string): Headers {
|
||||
const headers = new Headers({ "content-type": "application/json" });
|
||||
headers.set("idempotency-key", idempotencyKey);
|
||||
return headers;
|
||||
}
|
||||
|
||||
function parseOffer(value: unknown, index: number): SurplusOffer {
|
||||
const item = record(value, `Surplus offer ${index}`);
|
||||
const statusRaw = item.status;
|
||||
const status = statusRaw === "active" || item.active === true
|
||||
? "active"
|
||||
: statusRaw === "inactive" || statusRaw === "paused" || item.active === false
|
||||
? "inactive"
|
||||
: (() => { throw new Error(`Surplus offer ${index}.status was invalid`); })();
|
||||
return {
|
||||
id: offerId(item, `Surplus offer ${index}`),
|
||||
model: string(item.model, `Surplus offer ${index}.model`),
|
||||
sellerBaseUrl: string(item.seller_base_url, `Surplus offer ${index}.seller_base_url`),
|
||||
status,
|
||||
capDailyUsd: optionalNonNegativeNumber(item.cap_daily_usd, `Surplus offer ${index}.cap_daily_usd`),
|
||||
inputMicroUsdPer1m: optionalMicroUsd(item.price_input_per_1m, `Surplus offer ${index}.price_input_per_1m`),
|
||||
outputMicroUsdPer1m: optionalMicroUsd(item.price_output_per_1m, `Surplus offer ${index}.price_output_per_1m`),
|
||||
};
|
||||
}
|
||||
|
||||
function parseDiscoveredModel(value: unknown, index: number): SurplusDiscoveredModel {
|
||||
const item = record(value, `Surplus discovery item ${index}`);
|
||||
const pricing = record(item.pricing, `Surplus discovery item ${index}.pricing`);
|
||||
const metadata = record(item.metadata, `Surplus discovery item ${index}.metadata`);
|
||||
return {
|
||||
model: string(item.model, `Surplus discovery item ${index}.model`),
|
||||
supported: boolean(item.supported, `Surplus discovery item ${index}.supported`),
|
||||
inputUsdPer1m: nonNegativeNumber(pricing.input_per_1m, `Surplus discovery item ${index}.pricing.input_per_1m`),
|
||||
outputUsdPer1m: nonNegativeNumber(pricing.output_per_1m, `Surplus discovery item ${index}.pricing.output_per_1m`),
|
||||
priceUnit: optionalString(pricing.price_unit) ?? "M",
|
||||
priceVariable: pricing.price_variable === undefined ? false : boolean(pricing.price_variable, `Surplus discovery item ${index}.pricing.price_variable`),
|
||||
providerModelId: string(metadata.provider_model_id, `Surplus discovery item ${index}.metadata.provider_model_id`),
|
||||
modelType: string(metadata.model_type, `Surplus discovery item ${index}.metadata.model_type`),
|
||||
availabilityStatus: string(metadata.availability_status, `Surplus discovery item ${index}.metadata.availability_status`),
|
||||
};
|
||||
}
|
||||
|
||||
function parseMarketSummary(value: unknown, index: number): SurplusMarketSummary {
|
||||
const item = record(value, `Surplus market ${index}`);
|
||||
return {
|
||||
model: string(item.model, `Surplus market ${index}.model`),
|
||||
requests24h: nonNegativeSafeInteger(item.requests_24h, `Surplus market ${index}.requests_24h`),
|
||||
volume24h: nonNegativeSafeInteger(item.volume_24h, `Surplus market ${index}.volume_24h`),
|
||||
bestInputMicroUsdPer1m: nonNegativeSafeInteger(item.best_input_per_1m, `Surplus market ${index}.best_input_per_1m`),
|
||||
bestOutputMicroUsdPer1m: nonNegativeSafeInteger(item.best_output_per_1m, `Surplus market ${index}.best_output_per_1m`),
|
||||
healthySellerCount: nonNegativeSafeInteger(item.healthy_seller_count, `Surplus market ${index}.healthy_seller_count`),
|
||||
};
|
||||
}
|
||||
|
||||
function parseOrderBookOffer(value: unknown, index: number): SurplusOrderBookOffer {
|
||||
const item = record(value, `Surplus order book offer ${index}`);
|
||||
return {
|
||||
id: offerId(item, `Surplus order book offer ${index}`),
|
||||
seller: string(item.seller, `Surplus order book offer ${index}.seller`).toLowerCase(),
|
||||
sellerBaseUrl: string(item.seller_base_url, `Surplus order book offer ${index}.seller_base_url`),
|
||||
inputMicroUsdPer1m: positiveSafeInteger(item.effective_input_per_1m, `Surplus order book offer ${index}.effective_input_per_1m`),
|
||||
outputMicroUsdPer1m: positiveSafeInteger(item.effective_output_per_1m, `Surplus order book offer ${index}.effective_output_per_1m`),
|
||||
available: boolean(item.available, `Surplus order book offer ${index}.available`),
|
||||
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`),
|
||||
};
|
||||
}
|
||||
|
||||
function parseSale(value: unknown, index: number): SurplusSale {
|
||||
const item = record(value, `Surplus sale ${index}`);
|
||||
return {
|
||||
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),
|
||||
sellerCostMicroUsd: microUsd(item.seller_cost_usdc, `Surplus sale ${index}.seller_cost_usdc`),
|
||||
};
|
||||
}
|
||||
|
||||
function offerId(item: Record<string, unknown>, field: string): string {
|
||||
const value = item.offer_id ?? item.id;
|
||||
return string(value, `${field}.id`);
|
||||
}
|
||||
|
||||
function record(value: unknown, field: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error(`${field} must be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function string(value: unknown, field: string): string {
|
||||
if (typeof value !== "string" || value === "") throw new Error(`${field} must be a non-empty string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalString(value: unknown): string | null {
|
||||
return typeof value === "string" && value !== "" ? value : null;
|
||||
}
|
||||
|
||||
function boolean(value: unknown, field: string): boolean {
|
||||
if (typeof value !== "boolean") throw new Error(`${field} must be a boolean`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function nonNegativeNumber(value: unknown, field: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
throw new Error(`${field} must be a non-negative finite number`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalNonNegativeNumber(value: unknown, field: string): number | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
return nonNegativeNumber(value, field);
|
||||
}
|
||||
|
||||
function positiveSafeInteger(value: unknown, field: string): number {
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`${field} must be a positive safe integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function nonNegativeSafeInteger(value: unknown, field: string): number {
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
||||
throw new Error(`${field} must be a non-negative safe integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function microUsd(value: unknown, field: string): number {
|
||||
if (typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value)) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isSafeInteger(parsed)) return parsed;
|
||||
}
|
||||
return nonNegativeSafeInteger(value, field);
|
||||
}
|
||||
|
||||
function optionalMicroUsd(value: unknown, field: string): number | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
return microUsd(value, field);
|
||||
}
|
||||
|
||||
function sanitizedErrorSnippet(value: string): string {
|
||||
return value
|
||||
.slice(0, 300)
|
||||
.replace(/(?:si_seller_|sk-or-v1-)[A-Za-z0-9_-]+/g, "[redacted]")
|
||||
.replace(/[\r\n]+/g, " ");
|
||||
}
|
||||
202
supplier/src/surplus/config.ts
Normal file
202
supplier/src/surplus/config.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
const USD_RE = /^(?:0|[1-9]\d*)(?:\.\d{1,6})?$/;
|
||||
const EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
||||
const POSITIVE_INT_RE = /^[1-9]\d*$/;
|
||||
const NON_NEGATIVE_INT_RE = /^(?:0|[1-9]\d*)$/;
|
||||
|
||||
export interface SurplusManagerConfig {
|
||||
live: boolean;
|
||||
apiBaseUrl: string;
|
||||
sellerApiKey: string;
|
||||
providerApiKey: string;
|
||||
providerBaseUrl: string;
|
||||
capacityBaseUrl: string;
|
||||
perOfferCapUsd: string;
|
||||
aggregateCapUsd: string;
|
||||
sellerWallet: string;
|
||||
payoutAddress: string;
|
||||
maxCandidateOrderBooks: number;
|
||||
reserveUsd: string;
|
||||
maxProviderLimitUsd: string;
|
||||
recoveryBps: number;
|
||||
undercutBps: number;
|
||||
pollIntervalMs: number;
|
||||
requestTimeoutMs: number;
|
||||
stopAfterTrades: number;
|
||||
statePath: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export function loadSurplusManagerConfig(
|
||||
env: Record<string, string | undefined>,
|
||||
): SurplusManagerConfig {
|
||||
const liveRaw = env.SURPLUS_LIVE;
|
||||
if (liveRaw !== undefined && liveRaw !== "" && liveRaw !== "1") {
|
||||
throw new Error("SURPLUS_LIVE must be literal 1 or unset");
|
||||
}
|
||||
|
||||
const apiBaseUrl = httpsUrl(
|
||||
env.SURPLUS_API_URL ?? "https://api.surplusintelligence.ai",
|
||||
"SURPLUS_API_URL",
|
||||
"api.surplusintelligence.ai",
|
||||
);
|
||||
const providerBaseUrl = httpsUrl(
|
||||
env.SURPLUS_PROVIDER_BASE_URL ?? "https://openrouter.ai/api/v1",
|
||||
"SURPLUS_PROVIDER_BASE_URL",
|
||||
"openrouter.ai",
|
||||
);
|
||||
const capacityBaseUrl = httpsUrl(
|
||||
env.OPENROUTER_CAPACITY_BASE_URL ?? "https://openrouter.ai/api",
|
||||
"OPENROUTER_CAPACITY_BASE_URL",
|
||||
"openrouter.ai",
|
||||
);
|
||||
|
||||
const sellerWallet = evmAddress(
|
||||
required(env, "SURPLUS_SELLER_WALLET"),
|
||||
"SURPLUS_SELLER_WALLET",
|
||||
);
|
||||
const payoutAddress = evmAddress(
|
||||
env.SURPLUS_PAYOUT_ADDRESS ?? sellerWallet,
|
||||
"SURPLUS_PAYOUT_ADDRESS",
|
||||
);
|
||||
|
||||
return {
|
||||
live: liveRaw === "1",
|
||||
apiBaseUrl,
|
||||
sellerApiKey: prefixedSecret(
|
||||
required(env, "SURPLUS_SELLER_API_KEY"),
|
||||
"SURPLUS_SELLER_API_KEY",
|
||||
"si_seller_",
|
||||
),
|
||||
providerApiKey: required(env, "SURPLUS_OPENROUTER_API_KEY"),
|
||||
providerBaseUrl,
|
||||
capacityBaseUrl,
|
||||
perOfferCapUsd: usd(
|
||||
env.SURPLUS_PER_OFFER_CAP_USD ?? "1.00",
|
||||
"SURPLUS_PER_OFFER_CAP_USD",
|
||||
),
|
||||
aggregateCapUsd: usd(
|
||||
env.SURPLUS_AGGREGATE_CAP_USD ?? "1.00",
|
||||
"SURPLUS_AGGREGATE_CAP_USD",
|
||||
),
|
||||
sellerWallet,
|
||||
payoutAddress,
|
||||
maxCandidateOrderBooks: boundedInteger(
|
||||
env.SURPLUS_MAX_CANDIDATE_ORDER_BOOKS ?? "10",
|
||||
"SURPLUS_MAX_CANDIDATE_ORDER_BOOKS",
|
||||
1,
|
||||
100,
|
||||
),
|
||||
reserveUsd: usd(
|
||||
env.SURPLUS_OPENROUTER_RESERVE_USD ?? "1.00",
|
||||
"SURPLUS_OPENROUTER_RESERVE_USD",
|
||||
),
|
||||
maxProviderLimitUsd: usd(
|
||||
env.SURPLUS_OPENROUTER_MAX_LIMIT_USD ?? "20.00",
|
||||
"SURPLUS_OPENROUTER_MAX_LIMIT_USD",
|
||||
),
|
||||
recoveryBps: boundedInteger(
|
||||
env.SURPLUS_MIN_RECOVERY_BPS ?? "490",
|
||||
"SURPLUS_MIN_RECOVERY_BPS",
|
||||
1,
|
||||
10_000,
|
||||
),
|
||||
undercutBps: boundedInteger(
|
||||
env.SURPLUS_UNDERCUT_BPS ?? "10",
|
||||
"SURPLUS_UNDERCUT_BPS",
|
||||
0,
|
||||
9_999,
|
||||
),
|
||||
pollIntervalMs: boundedInteger(
|
||||
env.SURPLUS_REPRICE_INTERVAL_MS ?? "300000",
|
||||
"SURPLUS_REPRICE_INTERVAL_MS",
|
||||
60_000,
|
||||
86_400_000,
|
||||
),
|
||||
requestTimeoutMs: boundedInteger(
|
||||
env.SURPLUS_REQUEST_TIMEOUT_MS ?? "120000",
|
||||
"SURPLUS_REQUEST_TIMEOUT_MS",
|
||||
1_000,
|
||||
300_000,
|
||||
),
|
||||
stopAfterTrades: boundedInteger(
|
||||
env.SURPLUS_STOP_AFTER_SETTLED_REQUESTS ?? "1",
|
||||
"SURPLUS_STOP_AFTER_SETTLED_REQUESTS",
|
||||
1,
|
||||
1,
|
||||
),
|
||||
statePath: requiredWithDefault(
|
||||
env.SURPLUS_STATE_PATH,
|
||||
"/var/lib/surplus-manager/state.json",
|
||||
"SURPLUS_STATE_PATH",
|
||||
),
|
||||
port: boundedInteger(env.PORT ?? "8080", "PORT", 1, 65_535),
|
||||
};
|
||||
}
|
||||
|
||||
function required(
|
||||
env: Record<string, string | undefined>,
|
||||
name: string,
|
||||
): string {
|
||||
const value = env[name]?.trim();
|
||||
if (!value) throw new Error(`${name} is required`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredWithDefault(
|
||||
value: string | undefined,
|
||||
fallback: string,
|
||||
name: string,
|
||||
): string {
|
||||
const result = (value ?? fallback).trim();
|
||||
if (!result) throw new Error(`${name} must not be empty`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function prefixedSecret(value: string, name: string, prefix: string): string {
|
||||
if (!value.startsWith(prefix) || value.length <= prefix.length) {
|
||||
throw new Error(`${name} must start with ${prefix}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function usd(value: string, name: string): string {
|
||||
if (!USD_RE.test(value)) {
|
||||
throw new Error(`${name} must be a non-negative USD decimal with at most 6 places`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function evmAddress(value: string, name: string): string {
|
||||
if (!EVM_ADDRESS_RE.test(value)) {
|
||||
throw new Error(`${name} must be a 20-byte 0x address`);
|
||||
}
|
||||
return value.toLowerCase();
|
||||
}
|
||||
|
||||
function boundedInteger(
|
||||
value: string,
|
||||
name: string,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): number {
|
||||
const pattern = minimum === 0 ? NON_NEGATIVE_INT_RE : POSITIVE_INT_RE;
|
||||
if (!pattern.test(value)) throw new Error(`${name} must be an integer`);
|
||||
const parsed = Number(value);
|
||||
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
|
||||
throw new Error(`${name} must be from ${minimum} to ${maximum}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function httpsUrl(value: string, name: string, hostname: string): string {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(value);
|
||||
} catch {
|
||||
throw new Error(`${name} must be a valid URL`);
|
||||
}
|
||||
if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== hostname) {
|
||||
throw new Error(`${name} must use https://${hostname}`);
|
||||
}
|
||||
return value.replace(/\/+$/, "");
|
||||
}
|
||||
734
supplier/src/surplus/controller.ts
Normal file
734
supplier/src/surplus/controller.ts
Normal file
@@ -0,0 +1,734 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { parseUsdDecimal, formatUsdNanos } from "../reseller/money.js";
|
||||
import type { SurplusManagerConfig } from "./config.js";
|
||||
import {
|
||||
SurplusClient,
|
||||
SurplusHttpError,
|
||||
type SurplusDiscoveredModel,
|
||||
type SurplusEarnings,
|
||||
type SurplusOffer,
|
||||
} from "./client.js";
|
||||
import type {
|
||||
OpenRouterKeyAllowance,
|
||||
OpenRouterKeyClient,
|
||||
} from "./openRouterKey.js";
|
||||
import {
|
||||
formatMicroUsd,
|
||||
quoteSurplusPrice,
|
||||
} from "./policy.js";
|
||||
import {
|
||||
eligibleCompetitors,
|
||||
selectSurplusModelCandidates,
|
||||
} from "./selection.js";
|
||||
import type {
|
||||
SurplusControllerState,
|
||||
SurplusOfferIntent,
|
||||
SurplusStateStore,
|
||||
} from "./state.js";
|
||||
|
||||
export type SurplusControllerClient = Pick<
|
||||
SurplusClient,
|
||||
| "listAllOffers"
|
||||
| "discoverModels"
|
||||
| "getMarkets"
|
||||
| "getOrderBook"
|
||||
| "testConnection"
|
||||
| "createOffer"
|
||||
| "updateOffer"
|
||||
| "pauseOffer"
|
||||
| "resumeOffer"
|
||||
| "getEarnings"
|
||||
>;
|
||||
|
||||
export type OpenRouterAllowanceReader = Pick<OpenRouterKeyClient, "readAllowance">;
|
||||
|
||||
export interface SurplusControllerStatus {
|
||||
ok: boolean;
|
||||
phase: SurplusControllerState["phase"] | "error" | "planned" | "suspended";
|
||||
live: boolean;
|
||||
model: string | null;
|
||||
providerModelId: string | null;
|
||||
offerId: string | null;
|
||||
inputUsdPer1m: string | null;
|
||||
outputUsdPer1m: string | null;
|
||||
competitorId: string | null;
|
||||
remainingAllowanceUsd: string | null;
|
||||
reserveThresholdUsd: string;
|
||||
totalEarnedUsd: string | null;
|
||||
lastCycleAt: string | null;
|
||||
lastSuccessAt: string | null;
|
||||
lastError: string | null;
|
||||
}
|
||||
|
||||
interface SurplusControllerOptions {
|
||||
config: SurplusManagerConfig;
|
||||
client: SurplusControllerClient;
|
||||
allowance: OpenRouterAllowanceReader;
|
||||
stateStore: SurplusStateStore;
|
||||
now?: () => number;
|
||||
mutationId?: () => string;
|
||||
log?: (message: string) => void;
|
||||
}
|
||||
|
||||
const SETTLED_STATUSES: Record<string, true> = {
|
||||
confirmed: true,
|
||||
paid: true,
|
||||
settled: true,
|
||||
};
|
||||
|
||||
export class SurplusSellerController {
|
||||
private readonly config: SurplusManagerConfig;
|
||||
private readonly client: SurplusControllerClient;
|
||||
private readonly allowance: OpenRouterAllowanceReader;
|
||||
private readonly stateStore: SurplusStateStore;
|
||||
private readonly now: () => number;
|
||||
private readonly mutationId: () => string;
|
||||
private readonly log: (message: string) => void;
|
||||
private state: SurplusControllerState | null = null;
|
||||
private inFlight: Promise<void> | null = null;
|
||||
private timer: NodeJS.Timeout | null = null;
|
||||
private stopped = false;
|
||||
private status: SurplusControllerStatus;
|
||||
|
||||
constructor(options: SurplusControllerOptions) {
|
||||
this.config = options.config;
|
||||
this.client = options.client;
|
||||
this.allowance = options.allowance;
|
||||
this.stateStore = options.stateStore;
|
||||
this.now = options.now ?? Date.now;
|
||||
this.mutationId = options.mutationId ?? randomUUID;
|
||||
this.log = options.log ?? (() => undefined);
|
||||
const perOfferCap = parseUsdDecimal(this.config.perOfferCapUsd, "ceil");
|
||||
const aggregateCap = parseUsdDecimal(this.config.aggregateCapUsd, "floor");
|
||||
if (perOfferCap > aggregateCap) {
|
||||
throw new Error("Surplus per-offer cap must not exceed the aggregate cap");
|
||||
}
|
||||
this.status = {
|
||||
ok: false,
|
||||
phase: "selecting",
|
||||
live: this.config.live,
|
||||
model: null,
|
||||
providerModelId: null,
|
||||
offerId: null,
|
||||
inputUsdPer1m: null,
|
||||
outputUsdPer1m: null,
|
||||
competitorId: null,
|
||||
remainingAllowanceUsd: null,
|
||||
reserveThresholdUsd: this.config.reserveUsd,
|
||||
totalEarnedUsd: null,
|
||||
lastCycleAt: null,
|
||||
lastSuccessAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
}
|
||||
|
||||
snapshot(): SurplusControllerStatus {
|
||||
return { ...this.status };
|
||||
}
|
||||
|
||||
healthy(): boolean {
|
||||
if (this.state?.phase === "completed") return true;
|
||||
if (!this.status.ok || this.status.lastSuccessAt === null) return false;
|
||||
const freshnessLimit =
|
||||
this.config.pollIntervalMs + this.config.requestTimeoutMs + 60_000;
|
||||
return this.now() - Date.parse(this.status.lastSuccessAt) <= freshnessLimit;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.state !== null) return;
|
||||
this.state = await this.stateStore.load();
|
||||
await this.runOnce();
|
||||
this.schedule();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.stopped = true;
|
||||
clearTimeout(this.timer ?? undefined);
|
||||
this.timer = null;
|
||||
if (this.inFlight) await this.inFlight;
|
||||
}
|
||||
|
||||
async runOnce(): Promise<void> {
|
||||
if (this.inFlight) return this.inFlight;
|
||||
const cycle = this.performCycle();
|
||||
this.inFlight = cycle;
|
||||
try {
|
||||
await cycle;
|
||||
} finally {
|
||||
this.inFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
private schedule(): void {
|
||||
if (this.stopped || this.state?.phase === "completed") return;
|
||||
this.timer = setTimeout(() => {
|
||||
void this.runOnce().finally(() => this.schedule());
|
||||
}, this.config.pollIntervalMs);
|
||||
this.timer.unref();
|
||||
}
|
||||
|
||||
private async performCycle(): Promise<void> {
|
||||
if (this.state === null) this.state = await this.stateStore.load();
|
||||
const cycleAt = new Date(this.now()).toISOString();
|
||||
this.status = { ...this.status, lastCycleAt: cycleAt };
|
||||
if (this.state.phase === "completed") {
|
||||
this.markSuccess("completed", null, null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (this.state.phase === "selecting") await this.selectAndCreate();
|
||||
else if (this.state.phase === "create_pending") await this.adoptPendingCreate();
|
||||
else if (this.state.phase === "active") await this.manageActiveOffer();
|
||||
else if (this.state.phase === "stopping") await this.finishStopping();
|
||||
else await this.awaitSettlement();
|
||||
} catch (error) {
|
||||
const stateAtFailure = this.state;
|
||||
let paused = false;
|
||||
if (
|
||||
this.config.live &&
|
||||
stateAtFailure !== null &&
|
||||
"offerId" in stateAtFailure
|
||||
) {
|
||||
try {
|
||||
await this.client.pauseOffer(stateAtFailure.offerId, this.mutationId());
|
||||
paused = true;
|
||||
} catch {
|
||||
paused = false;
|
||||
}
|
||||
}
|
||||
const detail = this.safeError(error);
|
||||
this.status = {
|
||||
...this.status,
|
||||
ok: false,
|
||||
phase: "error",
|
||||
lastError: paused ? `${detail}; managed offer paused` : detail,
|
||||
};
|
||||
this.log(`Surplus cycle failed: ${this.status.lastError}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async selectAndCreate(): Promise<void> {
|
||||
const offers = await this.client.listAllOffers();
|
||||
const active = offers.filter((offer) => offer.status === "active");
|
||||
if (active.length > 0) {
|
||||
await this.pauseOffers(active);
|
||||
this.markSuccess("suspended", null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
const allowance = await this.allowance.readAllowance();
|
||||
this.assertAllowance(allowance, parseUsdDecimal(this.config.aggregateCapUsd, "ceil"));
|
||||
const [discovered, markets] = await Promise.all([
|
||||
this.client.discoverModels(
|
||||
this.config.providerApiKey,
|
||||
this.config.providerBaseUrl,
|
||||
),
|
||||
this.client.getMarkets(),
|
||||
]);
|
||||
const candidates = selectSurplusModelCandidates(
|
||||
discovered,
|
||||
markets,
|
||||
this.config.recoveryBps,
|
||||
this.config.undercutBps,
|
||||
);
|
||||
const ownOfferIds = new Set(offers.map((offer) => offer.id));
|
||||
let intent: SurplusOfferIntent | null = null;
|
||||
let competitorId: string | null = null;
|
||||
for (const candidate of candidates.slice(0, this.config.maxCandidateOrderBooks)) {
|
||||
const book = await this.client.getOrderBook(candidate.model);
|
||||
const competitors = eligibleCompetitors(
|
||||
book.offers,
|
||||
this.config.sellerWallet,
|
||||
ownOfferIds,
|
||||
);
|
||||
const quote = quoteSurplusPrice({
|
||||
upstreamInputMicroUsdPer1m: candidate.upstreamInputMicroUsdPer1m,
|
||||
upstreamOutputMicroUsdPer1m: candidate.upstreamOutputMicroUsdPer1m,
|
||||
recoveryBps: this.config.recoveryBps,
|
||||
undercutBps: this.config.undercutBps,
|
||||
competitors,
|
||||
});
|
||||
if (!quote.competitive) continue;
|
||||
try {
|
||||
await this.client.testConnection(
|
||||
this.config.providerApiKey,
|
||||
this.config.providerBaseUrl,
|
||||
candidate.model,
|
||||
);
|
||||
} catch (error) {
|
||||
this.log(
|
||||
`Surplus candidate probe failed model=${candidate.model}: ${this.safeError(error)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
intent = {
|
||||
model: candidate.model,
|
||||
providerModelId: candidate.providerModelId,
|
||||
inputMicroUsdPer1m: quote.inputMicroUsdPer1m,
|
||||
outputMicroUsdPer1m: quote.outputMicroUsdPer1m,
|
||||
dailyCapUsd: Number(this.config.perOfferCapUsd),
|
||||
idempotencyKey: this.mutationId(),
|
||||
createdAt: new Date(this.now()).toISOString(),
|
||||
baselineTrades24h: 0,
|
||||
};
|
||||
competitorId = quote.competitorId;
|
||||
break;
|
||||
}
|
||||
if (intent === null) {
|
||||
throw new Error("No supported OpenRouter model has a safe competitive quote");
|
||||
}
|
||||
if (!this.config.live) {
|
||||
this.markSuccess("planned", allowance, {
|
||||
intent,
|
||||
offerId: null,
|
||||
competitorId,
|
||||
totalEarnedMicroUsd: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const pending: SurplusControllerState = {
|
||||
version: 1,
|
||||
phase: "create_pending",
|
||||
intent,
|
||||
};
|
||||
await this.stateStore.save(pending);
|
||||
this.state = pending;
|
||||
let createdOfferId: string | null = null;
|
||||
let postCreateActive: SurplusOffer[] = [];
|
||||
try {
|
||||
createdOfferId = await this.client.createOffer({
|
||||
model: intent.model,
|
||||
apiKey: this.config.providerApiKey,
|
||||
sellerBaseUrl: this.config.providerBaseUrl,
|
||||
inputUsdPer1m: intent.inputMicroUsdPer1m / 1_000_000,
|
||||
outputUsdPer1m: intent.outputMicroUsdPer1m / 1_000_000,
|
||||
dailyCapUsd: intent.dailyCapUsd,
|
||||
payoutAddress: this.config.payoutAddress,
|
||||
idempotencyKey: intent.idempotencyKey,
|
||||
});
|
||||
const postCreateOffers = await this.client.listAllOffers();
|
||||
postCreateActive = postCreateOffers.filter(
|
||||
(offer) => offer.status === "active",
|
||||
);
|
||||
const created = postCreateActive.find(
|
||||
(offer) => offer.id === createdOfferId,
|
||||
);
|
||||
if (
|
||||
!created ||
|
||||
created.model !== intent.model ||
|
||||
postCreateActive.length !== 1
|
||||
) {
|
||||
throw new Error(
|
||||
"Surplus create verification did not find exactly one expected active offer",
|
||||
);
|
||||
}
|
||||
this.assertAggregateCap(postCreateOffers);
|
||||
const activeState: SurplusControllerState = {
|
||||
version: 1,
|
||||
phase: "active",
|
||||
offerId: createdOfferId,
|
||||
intent,
|
||||
highestTrades24h: 0,
|
||||
};
|
||||
await this.stateStore.save(activeState);
|
||||
this.state = activeState;
|
||||
this.markSuccess("active", allowance, {
|
||||
intent,
|
||||
offerId: createdOfferId,
|
||||
competitorId,
|
||||
totalEarnedMicroUsd: null,
|
||||
});
|
||||
} catch (error) {
|
||||
if (createdOfferId !== null) {
|
||||
const ids = new Set(postCreateActive.map((offer) => offer.id));
|
||||
ids.add(createdOfferId);
|
||||
for (const id of ids) {
|
||||
await this.client.pauseOffer(id, this.mutationId()).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
if (error instanceof SurplusHttpError && !error.ambiguousMutation) {
|
||||
const selecting: SurplusControllerState = {
|
||||
version: 1,
|
||||
phase: "selecting",
|
||||
};
|
||||
await this.stateStore.save(selecting);
|
||||
this.state = selecting;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async adoptPendingCreate(): Promise<void> {
|
||||
if (this.state?.phase !== "create_pending") return;
|
||||
const pending = this.state;
|
||||
const offers = await this.client.listAllOffers();
|
||||
const active = offers.filter((offer) => offer.status === "active");
|
||||
const matches = active
|
||||
.filter((offer) =>
|
||||
offer.model === pending.intent.model &&
|
||||
normalizeUrl(offer.sellerBaseUrl) ===
|
||||
normalizeUrl(this.config.providerBaseUrl)
|
||||
)
|
||||
.sort((left, right) => left.id.localeCompare(right.id));
|
||||
if (matches.length === 0) {
|
||||
const inactiveMatch = offers.some((offer) =>
|
||||
offer.status === "inactive" &&
|
||||
offer.model === pending.intent.model &&
|
||||
normalizeUrl(offer.sellerBaseUrl) ===
|
||||
normalizeUrl(this.config.providerBaseUrl)
|
||||
);
|
||||
if (inactiveMatch && active.length === 0) {
|
||||
const selecting: SurplusControllerState = {
|
||||
version: 1,
|
||||
phase: "selecting",
|
||||
};
|
||||
await this.stateStore.save(selecting);
|
||||
this.state = selecting;
|
||||
this.markSuccess("suspended", null, null);
|
||||
return;
|
||||
}
|
||||
if (active.length > 0) await this.pauseOffers(active);
|
||||
throw new Error(
|
||||
"Create result is ambiguous and no matching offer is visible; refusing another POST",
|
||||
);
|
||||
}
|
||||
const primary = matches[0];
|
||||
await this.pauseOffers(active.filter((offer) => offer.id !== primary.id));
|
||||
const confirmedOffers = await this.client.listAllOffers();
|
||||
const confirmedActive = confirmedOffers.filter(
|
||||
(offer) => offer.status === "active",
|
||||
);
|
||||
if (confirmedActive.length !== 1 || confirmedActive[0].id !== primary.id) {
|
||||
await this.pauseOffers(confirmedActive);
|
||||
throw new Error("Adopted Surplus create did not reconcile to one active offer");
|
||||
}
|
||||
this.assertAggregateCap(confirmedOffers);
|
||||
const adopted: SurplusControllerState = {
|
||||
version: 1,
|
||||
phase: "active",
|
||||
offerId: primary.id,
|
||||
intent: pending.intent,
|
||||
highestTrades24h: pending.intent.baselineTrades24h,
|
||||
};
|
||||
try {
|
||||
await this.stateStore.save(adopted);
|
||||
} catch (error) {
|
||||
await this.client.pauseOffer(primary.id, this.mutationId()).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
this.state = adopted;
|
||||
this.markSuccess("active", null, {
|
||||
intent: adopted.intent,
|
||||
offerId: adopted.offerId,
|
||||
competitorId: null,
|
||||
totalEarnedMicroUsd: null,
|
||||
});
|
||||
}
|
||||
|
||||
private async manageActiveOffer(): Promise<void> {
|
||||
if (this.state?.phase !== "active") return;
|
||||
const currentState = this.state;
|
||||
const offers = await this.client.listAllOffers();
|
||||
const managed = offers.find((offer) => offer.id === currentState.offerId);
|
||||
if (!managed) throw new Error(`Managed Surplus offer is missing: ${currentState.offerId}`);
|
||||
const otherActive = offers.filter(
|
||||
(offer) => offer.status === "active" && offer.id !== currentState.offerId,
|
||||
);
|
||||
if (otherActive.length > 0) {
|
||||
await this.pauseOffers(otherActive);
|
||||
this.markSuccess("suspended", null, null);
|
||||
return;
|
||||
}
|
||||
this.assertAggregateCap(offers);
|
||||
const allowance = await this.allowance.readAllowance();
|
||||
this.assertAllowance(
|
||||
allowance,
|
||||
parseUsdDecimal(this.config.perOfferCapUsd, "ceil"),
|
||||
);
|
||||
const discovered = await this.client.discoverModels(
|
||||
this.config.providerApiKey,
|
||||
this.config.providerBaseUrl,
|
||||
);
|
||||
const selected = this.requireSelectedDiscovery(discovered, currentState.intent);
|
||||
const book = await this.client.getOrderBook(currentState.intent.model);
|
||||
const ownBookOffer = book.offers.find((offer) => offer.id === currentState.offerId);
|
||||
if (
|
||||
ownBookOffer &&
|
||||
ownBookOffer.trades24h >=
|
||||
currentState.highestTrades24h + this.config.stopAfterTrades
|
||||
) {
|
||||
const stopping: SurplusControllerState = {
|
||||
version: 1,
|
||||
phase: "stopping",
|
||||
offerId: currentState.offerId,
|
||||
intent: currentState.intent,
|
||||
highestTrades24h: ownBookOffer.trades24h,
|
||||
tradeObservedAt: new Date(this.now()).toISOString(),
|
||||
pauseIdempotencyKey: this.mutationId(),
|
||||
};
|
||||
await this.stateStore.save(stopping);
|
||||
this.state = stopping;
|
||||
await this.finishStopping();
|
||||
return;
|
||||
}
|
||||
|
||||
const ownIds = new Set(offers.map((offer) => offer.id));
|
||||
const competitors = eligibleCompetitors(
|
||||
book.offers,
|
||||
this.config.sellerWallet,
|
||||
ownIds,
|
||||
);
|
||||
const quote = quoteSurplusPrice({
|
||||
upstreamInputMicroUsdPer1m: Math.ceil(selected.inputUsdPer1m * 1_000_000),
|
||||
upstreamOutputMicroUsdPer1m: Math.ceil(selected.outputUsdPer1m * 1_000_000),
|
||||
recoveryBps: this.config.recoveryBps,
|
||||
undercutBps: this.config.undercutBps,
|
||||
competitors,
|
||||
});
|
||||
const nextIntent: SurplusOfferIntent = {
|
||||
...currentState.intent,
|
||||
inputMicroUsdPer1m: quote.inputMicroUsdPer1m,
|
||||
outputMicroUsdPer1m: quote.outputMicroUsdPer1m,
|
||||
};
|
||||
const priceChanged = managed.inputMicroUsdPer1m !== quote.inputMicroUsdPer1m ||
|
||||
managed.outputMicroUsdPer1m !== quote.outputMicroUsdPer1m ||
|
||||
managed.capDailyUsd !== Number(this.config.perOfferCapUsd);
|
||||
if (this.config.live && priceChanged) {
|
||||
await this.client.updateOffer(currentState.offerId, {
|
||||
inputUsdPer1m: quote.inputUsdPer1m,
|
||||
outputUsdPer1m: quote.outputUsdPer1m,
|
||||
dailyCapUsd: Number(this.config.perOfferCapUsd),
|
||||
idempotencyKey: this.mutationId(),
|
||||
});
|
||||
}
|
||||
if (this.config.live && managed.status === "inactive") {
|
||||
await this.client.resumeOffer(currentState.offerId, this.mutationId());
|
||||
}
|
||||
const activeState: SurplusControllerState = {
|
||||
...currentState,
|
||||
intent: nextIntent,
|
||||
highestTrades24h: ownBookOffer?.trades24h ?? currentState.highestTrades24h,
|
||||
};
|
||||
await this.stateStore.save(activeState);
|
||||
this.state = activeState;
|
||||
const earnings = await this.client.getEarnings();
|
||||
this.markSuccess("active", allowance, {
|
||||
intent: nextIntent,
|
||||
offerId: currentState.offerId,
|
||||
competitorId: quote.competitorId,
|
||||
totalEarnedMicroUsd: earnings.totalEarnedMicroUsd,
|
||||
});
|
||||
}
|
||||
|
||||
private async finishStopping(): Promise<void> {
|
||||
if (this.state?.phase !== "stopping") return;
|
||||
const stopping = this.state;
|
||||
const before = await this.client.listAllOffers();
|
||||
const active = before.filter((offer) => offer.status === "active");
|
||||
for (const offer of active) {
|
||||
const key = offer.id === stopping.offerId
|
||||
? stopping.pauseIdempotencyKey
|
||||
: this.mutationId();
|
||||
await this.client.pauseOffer(offer.id, key);
|
||||
}
|
||||
const after = await this.client.listAllOffers();
|
||||
if (after.some((offer) => offer.status === "active")) {
|
||||
throw new Error("Surplus proof stop could not confirm all offers inactive");
|
||||
}
|
||||
const awaiting: SurplusControllerState = {
|
||||
version: 1,
|
||||
phase: "awaiting_settlement",
|
||||
offerId: stopping.offerId,
|
||||
intent: stopping.intent,
|
||||
tradeObservedAt: stopping.tradeObservedAt,
|
||||
};
|
||||
await this.stateStore.save(awaiting);
|
||||
this.state = awaiting;
|
||||
await this.awaitSettlement();
|
||||
}
|
||||
|
||||
private async awaitSettlement(): Promise<void> {
|
||||
if (this.state?.phase !== "awaiting_settlement") return;
|
||||
const awaiting = this.state;
|
||||
const offers = await this.client.listAllOffers();
|
||||
const active = offers.filter((offer) => offer.status === "active");
|
||||
if (active.length > 0) {
|
||||
await this.pauseOffers(active);
|
||||
this.markSuccess("suspended", null, null);
|
||||
return;
|
||||
}
|
||||
const earnings = await this.client.getEarnings();
|
||||
const sale = this.findSettledSale(earnings, awaiting);
|
||||
if (!sale) {
|
||||
this.markSuccess("awaiting_settlement", null, {
|
||||
intent: awaiting.intent,
|
||||
offerId: awaiting.offerId,
|
||||
competitorId: null,
|
||||
totalEarnedMicroUsd: earnings.totalEarnedMicroUsd,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const completed: SurplusControllerState = {
|
||||
version: 1,
|
||||
phase: "completed",
|
||||
offerId: awaiting.offerId,
|
||||
model: awaiting.intent.model,
|
||||
providerModelId: awaiting.intent.providerModelId,
|
||||
tradeObservedAt: awaiting.tradeObservedAt,
|
||||
completedAt: new Date(this.now()).toISOString(),
|
||||
settlement: {
|
||||
offerId: sale.offerId,
|
||||
createdAt: sale.createdAt ?? awaiting.tradeObservedAt,
|
||||
sellerCostMicroUsd: sale.sellerCostMicroUsd,
|
||||
settlementStatus: sale.settlementStatus,
|
||||
},
|
||||
};
|
||||
await this.stateStore.save(completed);
|
||||
this.state = completed;
|
||||
this.markSuccess("completed", null, {
|
||||
intent: awaiting.intent,
|
||||
offerId: awaiting.offerId,
|
||||
competitorId: null,
|
||||
totalEarnedMicroUsd: earnings.totalEarnedMicroUsd,
|
||||
});
|
||||
}
|
||||
|
||||
private findSettledSale(
|
||||
earnings: SurplusEarnings,
|
||||
state: Extract<SurplusControllerState, { phase: "awaiting_settlement" }>,
|
||||
): SurplusEarnings["recentSales"][number] | null {
|
||||
const tradeObservedAt = Date.parse(state.tradeObservedAt);
|
||||
return earnings.recentSales.find((sale) => {
|
||||
if (!SETTLED_STATUSES[sale.settlementStatus.toLowerCase()]) return false;
|
||||
if (sale.model !== state.intent.model || sale.sellerCostMicroUsd <= 0) return false;
|
||||
if (sale.offerId !== null) return sale.offerId === state.offerId;
|
||||
return sale.createdAt !== null &&
|
||||
Date.parse(sale.createdAt) >= tradeObservedAt;
|
||||
}) ?? null;
|
||||
}
|
||||
|
||||
private requireSelectedDiscovery(
|
||||
discovered: SurplusDiscoveredModel[],
|
||||
intent: SurplusOfferIntent,
|
||||
): SurplusDiscoveredModel {
|
||||
const selected = discovered.find((model) =>
|
||||
model.model === intent.model && model.providerModelId === intent.providerModelId
|
||||
);
|
||||
if (
|
||||
!selected ||
|
||||
!selected.supported ||
|
||||
selected.availabilityStatus !== "available" ||
|
||||
selected.modelType !== "text" ||
|
||||
selected.priceUnit !== "M" ||
|
||||
selected.priceVariable ||
|
||||
selected.inputUsdPer1m <= 0 ||
|
||||
selected.outputUsdPer1m <= 0
|
||||
) {
|
||||
throw new Error(`Selected OpenRouter model is no longer safely sellable: ${intent.model}`);
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private assertAggregateCap(offers: SurplusOffer[]): void {
|
||||
let activeCap = 0n;
|
||||
for (const offer of offers) {
|
||||
if (offer.status !== "active") continue;
|
||||
if (offer.capDailyUsd === null) {
|
||||
throw new Error(`Active Surplus offer has no daily cap: ${offer.id}`);
|
||||
}
|
||||
activeCap += parseUsdDecimal(offer.capDailyUsd.toFixed(6), "ceil");
|
||||
}
|
||||
const aggregateCap = parseUsdDecimal(this.config.aggregateCapUsd, "floor");
|
||||
if (activeCap > aggregateCap) {
|
||||
throw new Error("Active Surplus offer caps exceed the aggregate cap");
|
||||
}
|
||||
}
|
||||
|
||||
private assertAllowance(
|
||||
allowance: OpenRouterKeyAllowance,
|
||||
exposureUsdNanos: bigint,
|
||||
): void {
|
||||
const reserve = parseUsdDecimal(this.config.reserveUsd, "ceil");
|
||||
const maxLimit = parseUsdDecimal(this.config.maxProviderLimitUsd, "floor");
|
||||
if (
|
||||
allowance.limitUsdNanos > maxLimit ||
|
||||
allowance.remainingUsdNanos > allowance.limitUsdNanos
|
||||
) {
|
||||
throw new Error("OpenRouter key limit exceeds the configured containment limit");
|
||||
}
|
||||
if (allowance.remainingUsdNanos < reserve + exposureUsdNanos) {
|
||||
throw new Error("OpenRouter allowance cannot cover reserve threshold plus exposure");
|
||||
}
|
||||
}
|
||||
|
||||
private async pauseOffers(offers: SurplusOffer[]): Promise<void> {
|
||||
if (!this.config.live) return;
|
||||
for (const offer of offers) {
|
||||
await this.client.pauseOffer(offer.id, this.mutationId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private safeError(error: unknown): string {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
return detail
|
||||
.replaceAll(this.config.sellerApiKey, "[redacted]")
|
||||
.replaceAll(this.config.providerApiKey, "[redacted]")
|
||||
.replace(/(?:si_seller_|sk-or-v1-)[A-Za-z0-9_-]+/g, "[redacted]")
|
||||
.slice(0, 500);
|
||||
}
|
||||
|
||||
private markSuccess(
|
||||
phase: SurplusControllerStatus["phase"],
|
||||
allowance: OpenRouterKeyAllowance | null,
|
||||
details: {
|
||||
intent: SurplusOfferIntent;
|
||||
offerId: string | null;
|
||||
competitorId: string | null;
|
||||
totalEarnedMicroUsd: number | null;
|
||||
} | null,
|
||||
): void {
|
||||
const successAt = new Date(this.now()).toISOString();
|
||||
const state = this.state;
|
||||
const stateModel = state && "intent" in state
|
||||
? state.intent.model
|
||||
: state?.phase === "completed"
|
||||
? state.model
|
||||
: null;
|
||||
const stateProviderModel = state && "intent" in state
|
||||
? state.intent.providerModelId
|
||||
: state?.phase === "completed"
|
||||
? state.providerModelId
|
||||
: null;
|
||||
const stateOfferId = state && "offerId" in state ? state.offerId : null;
|
||||
this.status = {
|
||||
...this.status,
|
||||
ok: true,
|
||||
phase,
|
||||
model: details?.intent.model ?? stateModel,
|
||||
providerModelId: details?.intent.providerModelId ?? stateProviderModel,
|
||||
offerId: details?.offerId ?? stateOfferId,
|
||||
inputUsdPer1m: details
|
||||
? formatMicroUsd(details.intent.inputMicroUsdPer1m)
|
||||
: this.status.inputUsdPer1m,
|
||||
outputUsdPer1m: details
|
||||
? formatMicroUsd(details.intent.outputMicroUsdPer1m)
|
||||
: this.status.outputUsdPer1m,
|
||||
competitorId: details?.competitorId ?? null,
|
||||
remainingAllowanceUsd: allowance
|
||||
? formatUsdNanos(allowance.remainingUsdNanos, 6)
|
||||
: this.status.remainingAllowanceUsd,
|
||||
totalEarnedUsd: details?.totalEarnedMicroUsd === null || details === null
|
||||
? this.status.totalEarnedUsd
|
||||
: formatMicroUsd(details.totalEarnedMicroUsd),
|
||||
lastSuccessAt: successAt,
|
||||
lastError: null,
|
||||
};
|
||||
this.log(`Surplus controller phase=${phase} model=${this.status.model ?? "none"}`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUrl(value: string): string {
|
||||
return value.replace(/\/+$/, "").toLowerCase();
|
||||
}
|
||||
129
supplier/src/surplus/openRouterKey.ts
Normal file
129
supplier/src/surplus/openRouterKey.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { parseUsdDecimal } from "../reseller/money.js";
|
||||
|
||||
const MAX_KEY_RESPONSE_BYTES = 1024 * 1024;
|
||||
|
||||
export interface OpenRouterKeyAllowance {
|
||||
limitUsdNanos: bigint;
|
||||
remainingUsdNanos: bigint;
|
||||
reset: string | null;
|
||||
checkedAtMs: number;
|
||||
}
|
||||
|
||||
interface OpenRouterKeyClientOptions {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
timeoutMs: number;
|
||||
fetchFn?: typeof globalThis.fetch;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
export class OpenRouterKeyClient {
|
||||
private readonly url: string;
|
||||
private readonly apiKey: string;
|
||||
private readonly timeoutMs: number;
|
||||
private readonly fetchFn: typeof globalThis.fetch;
|
||||
private readonly now: () => number;
|
||||
|
||||
constructor(options: OpenRouterKeyClientOptions) {
|
||||
this.url = `${options.baseUrl.replace(/\/+$/, "")}/v1/auth/key`;
|
||||
this.apiKey = options.apiKey;
|
||||
this.timeoutMs = options.timeoutMs;
|
||||
this.fetchFn = options.fetchFn ?? globalThis.fetch;
|
||||
this.now = options.now ?? Date.now;
|
||||
}
|
||||
|
||||
async readAllowance(): Promise<OpenRouterKeyAllowance> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
try {
|
||||
const response = await this.fetchFn(this.url, {
|
||||
headers: { authorization: `Bearer ${this.apiKey}` },
|
||||
redirect: "error",
|
||||
signal: controller.signal,
|
||||
});
|
||||
const text = await readBoundedText(response, controller);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`OpenRouter key endpoint returned HTTP ${response.status}${
|
||||
text ? `: ${text.slice(0, 200).replace(/[\r\n]+/g, " ")}` : ""
|
||||
}`,
|
||||
);
|
||||
}
|
||||
let body: unknown;
|
||||
try {
|
||||
body = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error("OpenRouter key endpoint response was not JSON");
|
||||
}
|
||||
const root = object(body, "OpenRouter key response");
|
||||
const data = object(root.data, "OpenRouter key response.data");
|
||||
if (data.limit === null || data.limit_remaining === null) {
|
||||
throw new Error("OpenRouter resale key must have a finite spending limit");
|
||||
}
|
||||
const limit = usdNumberToNanos(data.limit, "OpenRouter key limit");
|
||||
const remaining = usdNumberToNanos(
|
||||
data.limit_remaining,
|
||||
"OpenRouter key limit_remaining",
|
||||
);
|
||||
const reset = data.limit_reset;
|
||||
if (reset !== null && reset !== undefined && typeof reset !== "string") {
|
||||
throw new Error("OpenRouter key limit_reset must be a string or null");
|
||||
}
|
||||
return {
|
||||
limitUsdNanos: limit,
|
||||
remainingUsdNanos: remaining,
|
||||
reset: typeof reset === "string" ? reset : null,
|
||||
checkedAtMs: this.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(
|
||||
`OpenRouter allowance check failed: ${detail.replaceAll(this.apiKey, "[redacted]")}`,
|
||||
);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readBoundedText(
|
||||
response: Response,
|
||||
controller: AbortController,
|
||||
): Promise<string> {
|
||||
if (!response.body) return "";
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let bytes = 0;
|
||||
let text = "";
|
||||
try {
|
||||
for (;;) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
bytes += chunk.value.byteLength;
|
||||
if (bytes > MAX_KEY_RESPONSE_BYTES) {
|
||||
controller.abort();
|
||||
throw new Error(
|
||||
`OpenRouter key response exceeded ${MAX_KEY_RESPONSE_BYTES} bytes`,
|
||||
);
|
||||
}
|
||||
text += decoder.decode(chunk.value, { stream: true });
|
||||
}
|
||||
return text + decoder.decode();
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
function usdNumberToNanos(value: unknown, field: string): bigint {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
throw new Error(`${field} must be a non-negative finite number`);
|
||||
}
|
||||
return parseUsdDecimal(value.toFixed(9), "floor");
|
||||
}
|
||||
|
||||
function object(value: unknown, field: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error(`${field} must be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
150
supplier/src/surplus/policy.ts
Normal file
150
supplier/src/surplus/policy.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
export interface SurplusCompetitor {
|
||||
id: string;
|
||||
inputMicroUsdPer1m: number;
|
||||
outputMicroUsdPer1m: number;
|
||||
}
|
||||
|
||||
export interface SurplusPricePolicyInput {
|
||||
upstreamInputMicroUsdPer1m: number;
|
||||
upstreamOutputMicroUsdPer1m: number;
|
||||
recoveryBps: number;
|
||||
undercutBps: number;
|
||||
competitors: SurplusCompetitor[];
|
||||
}
|
||||
|
||||
export interface SurplusPriceQuote {
|
||||
inputMicroUsdPer1m: number;
|
||||
outputMicroUsdPer1m: number;
|
||||
inputUsdPer1m: number;
|
||||
outputUsdPer1m: number;
|
||||
floorInputMicroUsdPer1m: number;
|
||||
floorOutputMicroUsdPer1m: number;
|
||||
competitorId: string | null;
|
||||
competitive: boolean;
|
||||
}
|
||||
|
||||
const BPS_SCALE = 10_000n;
|
||||
const MICRO_USD = 1_000_000;
|
||||
|
||||
export function quoteSurplusPrice(
|
||||
input: SurplusPricePolicyInput,
|
||||
): SurplusPriceQuote {
|
||||
positiveSafeInteger(
|
||||
input.upstreamInputMicroUsdPer1m,
|
||||
"upstream input price",
|
||||
);
|
||||
positiveSafeInteger(
|
||||
input.upstreamOutputMicroUsdPer1m,
|
||||
"upstream output price",
|
||||
);
|
||||
boundedBps(input.recoveryBps, "recoveryBps", 1, 10_000);
|
||||
boundedBps(input.undercutBps, "undercutBps", 0, 9_999);
|
||||
|
||||
const floorInput = multiplyBpsCeil(
|
||||
input.upstreamInputMicroUsdPer1m,
|
||||
input.recoveryBps,
|
||||
);
|
||||
const floorOutput = multiplyBpsCeil(
|
||||
input.upstreamOutputMicroUsdPer1m,
|
||||
input.recoveryBps,
|
||||
);
|
||||
|
||||
const competitor = cheapestCompetitor(input.competitors);
|
||||
const candidateInput = competitor
|
||||
? undercut(competitor.inputMicroUsdPer1m, input.undercutBps)
|
||||
: floorInput;
|
||||
const candidateOutput = competitor
|
||||
? undercut(competitor.outputMicroUsdPer1m, input.undercutBps)
|
||||
: floorOutput;
|
||||
const quotedInput = Math.max(floorInput, candidateInput);
|
||||
const quotedOutput = Math.max(floorOutput, candidateOutput);
|
||||
|
||||
return {
|
||||
inputMicroUsdPer1m: quotedInput,
|
||||
outputMicroUsdPer1m: quotedOutput,
|
||||
inputUsdPer1m: quotedInput / MICRO_USD,
|
||||
outputUsdPer1m: quotedOutput / MICRO_USD,
|
||||
floorInputMicroUsdPer1m: floorInput,
|
||||
floorOutputMicroUsdPer1m: floorOutput,
|
||||
competitorId: competitor?.id ?? null,
|
||||
competitive: competitor === null || (
|
||||
quotedInput < competitor.inputMicroUsdPer1m &&
|
||||
quotedOutput < competitor.outputMicroUsdPer1m
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function usdPer1mToMicroUsd(value: unknown, field: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
throw new Error(`${field} must be a positive finite number`);
|
||||
}
|
||||
const result = Math.ceil(value * MICRO_USD);
|
||||
positiveSafeInteger(result, field);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function formatMicroUsd(value: number): string {
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new Error("micro-USD value must be a non-negative safe integer");
|
||||
}
|
||||
const whole = Math.floor(value / MICRO_USD);
|
||||
const fraction = (value % MICRO_USD).toString().padStart(6, "0");
|
||||
return `${whole}.${fraction}`;
|
||||
}
|
||||
|
||||
function cheapestCompetitor(
|
||||
competitors: SurplusCompetitor[],
|
||||
): SurplusCompetitor | null {
|
||||
let result: SurplusCompetitor | null = null;
|
||||
for (const competitor of competitors) {
|
||||
positiveSafeInteger(competitor.inputMicroUsdPer1m, "competitor input price");
|
||||
positiveSafeInteger(competitor.outputMicroUsdPer1m, "competitor output price");
|
||||
if (result === null || compareCompetitors(competitor, result) < 0) {
|
||||
result = competitor;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function compareCompetitors(
|
||||
left: SurplusCompetitor,
|
||||
right: SurplusCompetitor,
|
||||
): number {
|
||||
return (
|
||||
left.inputMicroUsdPer1m + left.outputMicroUsdPer1m -
|
||||
(right.inputMicroUsdPer1m + right.outputMicroUsdPer1m) ||
|
||||
left.inputMicroUsdPer1m - right.inputMicroUsdPer1m ||
|
||||
left.outputMicroUsdPer1m - right.outputMicroUsdPer1m ||
|
||||
left.id.localeCompare(right.id)
|
||||
);
|
||||
}
|
||||
|
||||
function undercut(value: number, bps: number): number {
|
||||
const discounted = Number(
|
||||
(BigInt(value) * BigInt(10_000 - bps)) / BPS_SCALE,
|
||||
);
|
||||
if (discounted >= value && value > 1) return value - 1;
|
||||
return Math.max(1, discounted);
|
||||
}
|
||||
|
||||
function multiplyBpsCeil(value: number, bps: number): number {
|
||||
const numerator = BigInt(value) * BigInt(bps);
|
||||
return Number((numerator + BPS_SCALE - 1n) / BPS_SCALE);
|
||||
}
|
||||
|
||||
function positiveSafeInteger(value: number, field: string): void {
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`${field} must be a positive safe integer`);
|
||||
}
|
||||
}
|
||||
|
||||
function boundedBps(
|
||||
value: number,
|
||||
field: string,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): void {
|
||||
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
||||
throw new Error(`${field} must be from ${minimum} to ${maximum}`);
|
||||
}
|
||||
}
|
||||
110
supplier/src/surplus/selection.ts
Normal file
110
supplier/src/surplus/selection.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type {
|
||||
SurplusDiscoveredModel,
|
||||
SurplusMarketSummary,
|
||||
SurplusOrderBookOffer,
|
||||
} from "./client.js";
|
||||
import {
|
||||
quoteSurplusPrice,
|
||||
usdPer1mToMicroUsd,
|
||||
type SurplusCompetitor,
|
||||
type SurplusPriceQuote,
|
||||
} from "./policy.js";
|
||||
|
||||
export interface SurplusModelCandidate {
|
||||
model: string;
|
||||
providerModelId: string;
|
||||
upstreamInputMicroUsdPer1m: number;
|
||||
upstreamOutputMicroUsdPer1m: number;
|
||||
requests24h: number;
|
||||
volume24h: number;
|
||||
summaryQuote: SurplusPriceQuote;
|
||||
}
|
||||
|
||||
export function selectSurplusModelCandidates(
|
||||
discovered: SurplusDiscoveredModel[],
|
||||
markets: SurplusMarketSummary[],
|
||||
recoveryBps: number,
|
||||
undercutBps: number,
|
||||
): SurplusModelCandidate[] {
|
||||
const marketByModel = new Map(markets.map((market) => [market.model, market]));
|
||||
const candidates: SurplusModelCandidate[] = [];
|
||||
for (const model of discovered) {
|
||||
const market = marketByModel.get(model.model);
|
||||
if (
|
||||
!market ||
|
||||
!model.supported ||
|
||||
model.availabilityStatus !== "available" ||
|
||||
model.modelType !== "text" ||
|
||||
model.priceUnit !== "M" ||
|
||||
model.priceVariable ||
|
||||
model.inputUsdPer1m <= 0 ||
|
||||
model.outputUsdPer1m <= 0 ||
|
||||
market.healthySellerCount <= 0 ||
|
||||
market.bestInputMicroUsdPer1m <= 0 ||
|
||||
market.bestOutputMicroUsdPer1m <= 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const inputPrice = usdPer1mToMicroUsd(
|
||||
model.inputUsdPer1m,
|
||||
`${model.model} discovery input price`,
|
||||
);
|
||||
const outputPrice = usdPer1mToMicroUsd(
|
||||
model.outputUsdPer1m,
|
||||
`${model.model} discovery output price`,
|
||||
);
|
||||
const quote = quoteSurplusPrice({
|
||||
upstreamInputMicroUsdPer1m: inputPrice,
|
||||
upstreamOutputMicroUsdPer1m: outputPrice,
|
||||
recoveryBps,
|
||||
undercutBps,
|
||||
competitors: [{
|
||||
id: `summary:${model.model}`,
|
||||
inputMicroUsdPer1m: market.bestInputMicroUsdPer1m,
|
||||
outputMicroUsdPer1m: market.bestOutputMicroUsdPer1m,
|
||||
}],
|
||||
});
|
||||
if (!quote.competitive) continue;
|
||||
candidates.push({
|
||||
model: model.model,
|
||||
providerModelId: model.providerModelId,
|
||||
upstreamInputMicroUsdPer1m: inputPrice,
|
||||
upstreamOutputMicroUsdPer1m: outputPrice,
|
||||
requests24h: market.requests24h,
|
||||
volume24h: market.volume24h,
|
||||
summaryQuote: quote,
|
||||
});
|
||||
}
|
||||
return candidates.sort(compareCandidates);
|
||||
}
|
||||
|
||||
export function eligibleCompetitors(
|
||||
offers: SurplusOrderBookOffer[],
|
||||
sellerWallet: string,
|
||||
ownOfferIds: ReadonlySet<string>,
|
||||
): SurplusCompetitor[] {
|
||||
const normalizedWallet = sellerWallet.toLowerCase();
|
||||
return offers
|
||||
.filter((offer) =>
|
||||
offer.available &&
|
||||
offer.healthy &&
|
||||
offer.trusted &&
|
||||
offer.seller !== normalizedWallet &&
|
||||
!ownOfferIds.has(offer.id)
|
||||
)
|
||||
.map((offer) => ({
|
||||
id: offer.id,
|
||||
inputMicroUsdPer1m: offer.inputMicroUsdPer1m,
|
||||
outputMicroUsdPer1m: offer.outputMicroUsdPer1m,
|
||||
}));
|
||||
}
|
||||
|
||||
function compareCandidates(
|
||||
left: SurplusModelCandidate,
|
||||
right: SurplusModelCandidate,
|
||||
): number {
|
||||
return right.requests24h - left.requests24h ||
|
||||
right.volume24h - left.volume24h ||
|
||||
left.model.localeCompare(right.model) ||
|
||||
left.providerModelId.localeCompare(right.providerModelId);
|
||||
}
|
||||
338
supplier/src/surplus/state.ts
Normal file
338
supplier/src/surplus/state.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { constants, type Stats } from "node:fs";
|
||||
import {
|
||||
chmod,
|
||||
lstat,
|
||||
mkdir,
|
||||
open,
|
||||
rename,
|
||||
unlink,
|
||||
type FileHandle,
|
||||
} from "node:fs/promises";
|
||||
import { dirname, basename, join } from "node:path";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export interface SurplusOfferIntent {
|
||||
model: string;
|
||||
providerModelId: string;
|
||||
inputMicroUsdPer1m: number;
|
||||
outputMicroUsdPer1m: number;
|
||||
dailyCapUsd: number;
|
||||
idempotencyKey: string;
|
||||
createdAt: string;
|
||||
baselineTrades24h: number;
|
||||
}
|
||||
|
||||
export interface SurplusSettlementEvidence {
|
||||
offerId: string | null;
|
||||
createdAt: string;
|
||||
sellerCostMicroUsd: number;
|
||||
settlementStatus: string;
|
||||
}
|
||||
|
||||
export type SurplusControllerState =
|
||||
| { version: 1; phase: "selecting" }
|
||||
| { version: 1; phase: "create_pending"; intent: SurplusOfferIntent }
|
||||
| {
|
||||
version: 1;
|
||||
phase: "active";
|
||||
offerId: string;
|
||||
intent: SurplusOfferIntent;
|
||||
highestTrades24h: number;
|
||||
}
|
||||
| {
|
||||
version: 1;
|
||||
phase: "stopping";
|
||||
offerId: string;
|
||||
intent: SurplusOfferIntent;
|
||||
highestTrades24h: number;
|
||||
tradeObservedAt: string;
|
||||
pauseIdempotencyKey: string;
|
||||
}
|
||||
| {
|
||||
version: 1;
|
||||
phase: "awaiting_settlement";
|
||||
offerId: string;
|
||||
intent: SurplusOfferIntent;
|
||||
tradeObservedAt: string;
|
||||
}
|
||||
| {
|
||||
version: 1;
|
||||
phase: "completed";
|
||||
offerId: string;
|
||||
model: string;
|
||||
providerModelId: string;
|
||||
tradeObservedAt: string;
|
||||
completedAt: string;
|
||||
settlement: SurplusSettlementEvidence;
|
||||
};
|
||||
|
||||
export interface SurplusStateStore {
|
||||
load(): Promise<SurplusControllerState>;
|
||||
save(state: SurplusControllerState): Promise<void>;
|
||||
}
|
||||
|
||||
export class FileSurplusStateStore implements SurplusStateStore {
|
||||
private readonly path: string;
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
async load(): Promise<SurplusControllerState> {
|
||||
let handle: FileHandle;
|
||||
try {
|
||||
handle = await open(
|
||||
this.path,
|
||||
constants.O_RDONLY | constants.O_NOFOLLOW,
|
||||
);
|
||||
} catch (error) {
|
||||
if (isNodeError(error, "ENOENT")) return { version: 1, phase: "selecting" };
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
const info = await handle.stat();
|
||||
assertSecureOwner(info, false, "Surplus state file");
|
||||
const text = await handle.readFile("utf8");
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error("Surplus state file is not valid JSON");
|
||||
}
|
||||
return parseState(value);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
async save(state: SurplusControllerState): Promise<void> {
|
||||
const directory = dirname(this.path);
|
||||
await mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
await chmod(directory, 0o700);
|
||||
const directoryInfo = await lstat(directory);
|
||||
assertSecureOwner(directoryInfo, true, "Surplus state directory");
|
||||
const tempPath = join(directory, `.${basename(this.path)}.${randomUUID()}.tmp`);
|
||||
const body = `${JSON.stringify(state)}\n`;
|
||||
let handle: FileHandle | null = null;
|
||||
try {
|
||||
handle = await open(
|
||||
tempPath,
|
||||
constants.O_WRONLY |
|
||||
constants.O_CREAT |
|
||||
constants.O_EXCL |
|
||||
constants.O_NOFOLLOW,
|
||||
0o600,
|
||||
);
|
||||
await handle.writeFile(body, "utf8");
|
||||
await handle.sync();
|
||||
await handle.close();
|
||||
handle = null;
|
||||
await rename(tempPath, this.path);
|
||||
const directoryHandle = await open(
|
||||
directory,
|
||||
constants.O_RDONLY | constants.O_DIRECTORY,
|
||||
);
|
||||
try {
|
||||
await directoryHandle.sync();
|
||||
} finally {
|
||||
await directoryHandle.close();
|
||||
}
|
||||
} finally {
|
||||
if (handle !== null) await handle.close().catch(() => undefined);
|
||||
await unlink(tempPath).catch((error) => {
|
||||
if (!isNodeError(error, "ENOENT")) throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertSecureOwner(
|
||||
info: Stats,
|
||||
directory: boolean,
|
||||
field: string,
|
||||
): void {
|
||||
if (directory ? !info.isDirectory() : !info.isFile()) {
|
||||
throw new Error(`${field} has the wrong file type`);
|
||||
}
|
||||
if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
|
||||
throw new Error(`${field} must be owned by the service user`);
|
||||
}
|
||||
if ((info.mode & 0o077) !== 0) {
|
||||
throw new Error(`${field} must not allow group or other access`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseState(value: unknown): SurplusControllerState {
|
||||
const root = object(value, "Surplus state");
|
||||
if (root.version !== 1) throw new Error("Surplus state version must be 1");
|
||||
const phase = nonEmptyString(root.phase, "Surplus state phase");
|
||||
if (phase === "selecting") return { version: 1, phase };
|
||||
if (phase === "create_pending") {
|
||||
return { version: 1, phase, intent: parseIntent(root.intent) };
|
||||
}
|
||||
if (phase === "active") {
|
||||
return {
|
||||
version: 1,
|
||||
phase,
|
||||
offerId: nonEmptyString(root.offerId, "Surplus state offerId"),
|
||||
intent: parseIntent(root.intent),
|
||||
highestTrades24h: nonNegativeInteger(
|
||||
root.highestTrades24h,
|
||||
"Surplus state highestTrades24h",
|
||||
),
|
||||
};
|
||||
}
|
||||
if (phase === "stopping") {
|
||||
return {
|
||||
version: 1,
|
||||
phase,
|
||||
offerId: nonEmptyString(root.offerId, "Surplus state offerId"),
|
||||
intent: parseIntent(root.intent),
|
||||
highestTrades24h: nonNegativeInteger(
|
||||
root.highestTrades24h,
|
||||
"Surplus state highestTrades24h",
|
||||
),
|
||||
tradeObservedAt: isoTimestamp(
|
||||
root.tradeObservedAt,
|
||||
"Surplus state tradeObservedAt",
|
||||
),
|
||||
pauseIdempotencyKey: nonEmptyString(
|
||||
root.pauseIdempotencyKey,
|
||||
"Surplus state pauseIdempotencyKey",
|
||||
),
|
||||
};
|
||||
}
|
||||
if (phase === "awaiting_settlement") {
|
||||
return {
|
||||
version: 1,
|
||||
phase,
|
||||
offerId: nonEmptyString(root.offerId, "Surplus state offerId"),
|
||||
intent: parseIntent(root.intent),
|
||||
tradeObservedAt: isoTimestamp(
|
||||
root.tradeObservedAt,
|
||||
"Surplus state tradeObservedAt",
|
||||
),
|
||||
};
|
||||
}
|
||||
if (phase === "completed") {
|
||||
return {
|
||||
version: 1,
|
||||
phase,
|
||||
offerId: nonEmptyString(root.offerId, "Surplus state offerId"),
|
||||
model: nonEmptyString(root.model, "Surplus state model"),
|
||||
providerModelId: nonEmptyString(
|
||||
root.providerModelId,
|
||||
"Surplus state providerModelId",
|
||||
),
|
||||
tradeObservedAt: isoTimestamp(
|
||||
root.tradeObservedAt,
|
||||
"Surplus state tradeObservedAt",
|
||||
),
|
||||
completedAt: isoTimestamp(root.completedAt, "Surplus state completedAt"),
|
||||
settlement: parseSettlement(root.settlement),
|
||||
};
|
||||
}
|
||||
throw new Error(`Surplus state phase is unsupported: ${phase}`);
|
||||
}
|
||||
|
||||
function parseSettlement(value: unknown): SurplusSettlementEvidence {
|
||||
const settlement = object(value, "Surplus state settlement");
|
||||
const offerId = settlement.offerId;
|
||||
if (offerId !== null && (typeof offerId !== "string" || offerId === "")) {
|
||||
throw new Error("Surplus state settlement.offerId must be a string or null");
|
||||
}
|
||||
return {
|
||||
offerId,
|
||||
createdAt: isoTimestamp(
|
||||
settlement.createdAt,
|
||||
"Surplus state settlement.createdAt",
|
||||
),
|
||||
sellerCostMicroUsd: positiveInteger(
|
||||
settlement.sellerCostMicroUsd,
|
||||
"Surplus state settlement.sellerCostMicroUsd",
|
||||
),
|
||||
settlementStatus: nonEmptyString(
|
||||
settlement.settlementStatus,
|
||||
"Surplus state settlement.settlementStatus",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseIntent(value: unknown): SurplusOfferIntent {
|
||||
const intent = object(value, "Surplus state intent");
|
||||
return {
|
||||
model: nonEmptyString(intent.model, "Surplus state intent.model"),
|
||||
providerModelId: nonEmptyString(
|
||||
intent.providerModelId,
|
||||
"Surplus state intent.providerModelId",
|
||||
),
|
||||
inputMicroUsdPer1m: positiveInteger(
|
||||
intent.inputMicroUsdPer1m,
|
||||
"Surplus state intent.inputMicroUsdPer1m",
|
||||
),
|
||||
outputMicroUsdPer1m: positiveInteger(
|
||||
intent.outputMicroUsdPer1m,
|
||||
"Surplus state intent.outputMicroUsdPer1m",
|
||||
),
|
||||
dailyCapUsd: positiveNumber(
|
||||
intent.dailyCapUsd,
|
||||
"Surplus state intent.dailyCapUsd",
|
||||
),
|
||||
idempotencyKey: nonEmptyString(
|
||||
intent.idempotencyKey,
|
||||
"Surplus state intent.idempotencyKey",
|
||||
),
|
||||
createdAt: isoTimestamp(intent.createdAt, "Surplus state intent.createdAt"),
|
||||
baselineTrades24h: nonNegativeInteger(
|
||||
intent.baselineTrades24h,
|
||||
"Surplus state intent.baselineTrades24h",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function object(value: unknown, field: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error(`${field} must be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function nonEmptyString(value: unknown, field: string): string {
|
||||
if (typeof value !== "string" || value === "") {
|
||||
throw new Error(`${field} must be a non-empty string`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isoTimestamp(value: unknown, field: string): string {
|
||||
const timestamp = nonEmptyString(value, field);
|
||||
if (!Number.isFinite(Date.parse(timestamp))) {
|
||||
throw new Error(`${field} must be an ISO timestamp`);
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown, field: string): number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) <= 0) {
|
||||
throw new Error(`${field} must be a positive safe integer`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value: unknown, field: string): number {
|
||||
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
||||
throw new Error(`${field} must be a non-negative safe integer`);
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
function positiveNumber(value: unknown, field: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
throw new Error(`${field} must be a positive finite number`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isNodeError(error: unknown, code: string): boolean {
|
||||
return error instanceof Error && "code" in error && error.code === code;
|
||||
}
|
||||
Reference in New Issue
Block a user