Files
agents-marketplace/supplier/src/surplus/controller.ts
2026-08-27 12:51:15 +02:00

749 lines
25 KiB
TypeScript

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;
}
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");
}
const adopted: SurplusControllerState = {
version: 1,
phase: "active",
offerId: primary.id,
intent: pending.intent,
highestTrades24h: pending.intent.baselineTrades24h,
};
try {
this.assertAggregateCap(confirmedOffers);
await this.stateStore.save(adopted);
} catch (error) {
await this.pauseOffers(confirmedActive).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 activeOffers = offers.filter((offer) => offer.status === "active");
const managed = offers.find((offer) => offer.id === currentState.offerId);
if (!managed) {
await this.pauseOffers(activeOffers);
throw new Error(`Managed Surplus offer is missing: ${currentState.offerId}`);
}
const otherActive = activeOffers.filter(
(offer) => 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 (
!this.config.settledStatuses.includes(
sale.settlementStatus.toLowerCase(),
)
) {
return false;
}
if (
sale.offerId !== state.offerId ||
sale.createdAt === null ||
sale.model !== state.intent.model ||
sale.sellerCostMicroUsd <= 0
) {
return false;
}
return 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}`);
}
const capMicroUsd = offer.capDailyUsd * 1_000_000;
if (!Number.isSafeInteger(capMicroUsd)) {
throw new Error(`Active Surplus offer cap has excess precision: ${offer.id}`);
}
activeCap += BigInt(capMicroUsd) * 1_000n;
}
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();
}