fix: stop Surplus offer at provider spend cap
This commit is contained in:
@@ -262,6 +262,7 @@ export class SurplusSellerController {
|
|||||||
costMultiplierPpm: quote.costMultiplierPpm,
|
costMultiplierPpm: quote.costMultiplierPpm,
|
||||||
inputMicroUsdPer1m: quote.inputMicroUsdPer1m,
|
inputMicroUsdPer1m: quote.inputMicroUsdPer1m,
|
||||||
outputMicroUsdPer1m: quote.outputMicroUsdPer1m,
|
outputMicroUsdPer1m: quote.outputMicroUsdPer1m,
|
||||||
|
baselineRemainingUsdNanos: allowance.remainingUsdNanos.toString(),
|
||||||
dailyCapUsd: Number(this.config.perOfferCapUsd),
|
dailyCapUsd: Number(this.config.perOfferCapUsd),
|
||||||
idempotencyKey: this.mutationId(),
|
idempotencyKey: this.mutationId(),
|
||||||
createdAt: new Date(this.now()).toISOString(),
|
createdAt: new Date(this.now()).toISOString(),
|
||||||
@@ -445,6 +446,28 @@ export class SurplusSellerController {
|
|||||||
allowance,
|
allowance,
|
||||||
parseUsdDecimal(this.config.perOfferCapUsd, "ceil"),
|
parseUsdDecimal(this.config.perOfferCapUsd, "ceil"),
|
||||||
);
|
);
|
||||||
|
const baselineAllowance = BigInt(
|
||||||
|
currentState.intent.baselineRemainingUsdNanos,
|
||||||
|
);
|
||||||
|
const providerSpend = baselineAllowance > allowance.remainingUsdNanos
|
||||||
|
? baselineAllowance - allowance.remainingUsdNanos
|
||||||
|
: 0n;
|
||||||
|
const spendCap = parseUsdDecimal(this.config.perOfferCapUsd, "ceil");
|
||||||
|
if (providerSpend >= spendCap) {
|
||||||
|
const stopping: SurplusControllerState = {
|
||||||
|
version: 1,
|
||||||
|
phase: "stopping",
|
||||||
|
offerId: currentState.offerId,
|
||||||
|
intent: currentState.intent,
|
||||||
|
highestTrades24h: currentState.highestTrades24h,
|
||||||
|
tradeObservedAt: currentState.intent.createdAt,
|
||||||
|
pauseIdempotencyKey: this.mutationId(),
|
||||||
|
};
|
||||||
|
await this.stateStore.save(stopping);
|
||||||
|
this.state = stopping;
|
||||||
|
await this.finishStopping();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const discovered = await this.client.discoverModels(
|
const discovered = await this.client.discoverModels(
|
||||||
this.config.providerApiKey,
|
this.config.providerApiKey,
|
||||||
this.config.providerBaseUrl,
|
this.config.providerBaseUrl,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface SurplusOfferIntent {
|
|||||||
inputMicroUsdPer1m: number;
|
inputMicroUsdPer1m: number;
|
||||||
outputMicroUsdPer1m: number;
|
outputMicroUsdPer1m: number;
|
||||||
dailyCapUsd: number;
|
dailyCapUsd: number;
|
||||||
|
baselineRemainingUsdNanos: string;
|
||||||
idempotencyKey: string;
|
idempotencyKey: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
baselineTrades24h: number;
|
baselineTrades24h: number;
|
||||||
@@ -283,6 +284,10 @@ function parseIntent(value: unknown): SurplusOfferIntent {
|
|||||||
intent.dailyCapUsd,
|
intent.dailyCapUsd,
|
||||||
"Surplus state intent.dailyCapUsd",
|
"Surplus state intent.dailyCapUsd",
|
||||||
),
|
),
|
||||||
|
baselineRemainingUsdNanos: nonNegativeIntegerString(
|
||||||
|
intent.baselineRemainingUsdNanos,
|
||||||
|
"Surplus state intent.baselineRemainingUsdNanos",
|
||||||
|
),
|
||||||
idempotencyKey: nonEmptyString(
|
idempotencyKey: nonEmptyString(
|
||||||
intent.idempotencyKey,
|
intent.idempotencyKey,
|
||||||
"Surplus state intent.idempotencyKey",
|
"Surplus state intent.idempotencyKey",
|
||||||
@@ -309,6 +314,13 @@ function nonEmptyString(value: unknown, field: string): string {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nonNegativeIntegerString(value: unknown, field: string): string {
|
||||||
|
if (typeof value !== "string" || !/^(?:0|[1-9]\d*)$/.test(value)) {
|
||||||
|
throw new Error(`${field} must be a non-negative integer string`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function isoTimestamp(value: unknown, field: string): string {
|
function isoTimestamp(value: unknown, field: string): string {
|
||||||
const timestamp = nonEmptyString(value, field);
|
const timestamp = nonEmptyString(value, field);
|
||||||
if (!Number.isFinite(Date.parse(timestamp))) {
|
if (!Number.isFinite(Date.parse(timestamp))) {
|
||||||
|
|||||||
@@ -208,12 +208,15 @@ const ALLOWANCE: OpenRouterKeyAllowance = {
|
|||||||
function controller(
|
function controller(
|
||||||
client: FakeSurplusClient,
|
client: FakeSurplusClient,
|
||||||
store: MemoryStateStore,
|
store: MemoryStateStore,
|
||||||
|
allowance: { readAllowance(): Promise<OpenRouterKeyAllowance> } = {
|
||||||
|
readAllowance: async () => ALLOWANCE,
|
||||||
|
},
|
||||||
): SurplusSellerController {
|
): SurplusSellerController {
|
||||||
let mutation = 0;
|
let mutation = 0;
|
||||||
return new SurplusSellerController({
|
return new SurplusSellerController({
|
||||||
config: config(),
|
config: config(),
|
||||||
client,
|
client,
|
||||||
allowance: { readAllowance: async () => ALLOWANCE },
|
allowance,
|
||||||
stateStore: store,
|
stateStore: store,
|
||||||
now: () => Date.parse("2026-08-27T10:00:00.000Z"),
|
now: () => Date.parse("2026-08-27T10:00:00.000Z"),
|
||||||
mutationId: () => `mutation-${++mutation}`,
|
mutationId: () => `mutation-${++mutation}`,
|
||||||
@@ -260,6 +263,26 @@ describe("SurplusSellerController", () => {
|
|||||||
await restarted.stop();
|
await restarted.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("pauses when provider spend reaches the per-offer cap", async () => {
|
||||||
|
const client = new FakeSurplusClient();
|
||||||
|
const store = new MemoryStateStore();
|
||||||
|
let remaining = 20_000_000_000n;
|
||||||
|
const activeController = controller(client, store, {
|
||||||
|
readAllowance: async () => ({
|
||||||
|
...ALLOWANCE,
|
||||||
|
remainingUsdNanos: remaining,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
await activeController.runOnce();
|
||||||
|
expect(client.offer?.status).toBe("active");
|
||||||
|
|
||||||
|
remaining = 18_700_000_000n;
|
||||||
|
await activeController.runOnce();
|
||||||
|
|
||||||
|
expect(client.offer?.status).toBe("inactive");
|
||||||
|
expect(store.state.phase).toBe("awaiting_settlement");
|
||||||
|
});
|
||||||
|
|
||||||
it("adopts and resumes an inactive offer after an ambiguous create", async () => {
|
it("adopts and resumes an inactive offer after an ambiguous create", async () => {
|
||||||
const client = new FakeSurplusClient();
|
const client = new FakeSurplusClient();
|
||||||
client.offer = {
|
client.offer = {
|
||||||
@@ -283,6 +306,7 @@ describe("SurplusSellerController", () => {
|
|||||||
inputMicroUsdPer1m: 3_972,
|
inputMicroUsdPer1m: 3_972,
|
||||||
outputMicroUsdPer1m: 7_944,
|
outputMicroUsdPer1m: 7_944,
|
||||||
dailyCapUsd: 1,
|
dailyCapUsd: 1,
|
||||||
|
baselineRemainingUsdNanos: "20000000000",
|
||||||
idempotencyKey: "create-1",
|
idempotencyKey: "create-1",
|
||||||
createdAt: "2026-08-27T10:00:00.000Z",
|
createdAt: "2026-08-27T10:00:00.000Z",
|
||||||
baselineTrades24h: 0,
|
baselineTrades24h: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user