From 216d24e48e5232f6e55df48dac5778314a2758a5 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Tue, 4 Aug 2026 13:51:25 -0500 Subject: [PATCH] fix(pm): make the retired canonical-market helpers fail fast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream probe (api.predexon.com direct, 3 runs each, 2026-08-04): /v1/pm/markets, /v1/pm/markets/listings and /v1/pm/outcomes/{id} all return 410 "This endpoint has been sunset as of 2026-07-20. Market matching is discontinued." pmMarkets / pmListings / pmOutcome have been calling them ever since. Payment was never charged — the gateway returns before settlement — but the caller pays a round trip to learn the endpoint is gone, and the JSDoc still advertised them as live Tier 1 endpoints. Kept as throwing stubs rather than deleted: removing a public method breaks property access on upgrade, and this is a patch release. They now throw RetiredEndpointError (new, exported, extends BlockrunError so existing catch blocks still catch it) naming the sunset date and `pm("markets/search", { q })` as the replacement — before any network I/O. pmSportsCategories / pmSportsMarkets keep working but gain a note: every sports/* path is returning a consistent upstream 500 as of 2026-08-04. That is a partner bug, not a sunset, so the helpers stay callable for when it recovers. eslint gains the standard argsIgnorePattern: "^_" so a deliberately unused parameter can keep the signature intact without a disable comment. --- eslint.config.js | 31 ++++++++++------ src/client.ts | 56 +++++++++++++++++++++-------- src/index.ts | 1 + src/types.ts | 14 ++++++++ test/unit/retired-endpoints.test.ts | 41 +++++++++++++++++++++ 5 files changed, 119 insertions(+), 24 deletions(-) create mode 100644 test/unit/retired-endpoints.test.ts diff --git a/eslint.config.js b/eslint.config.js index 5fe454e..abd0cae 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,10 +1,21 @@ -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; - -export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, - { - ignores: ["dist/", "node_modules/"], - } -); +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + // Retired helpers keep their parameters so the public signature does not + // change, but never read them — the leading underscore is the standard + // way to say "deliberately unused". + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + }, + }, + { + ignores: ["dist/", "node_modules/"], + } +); diff --git a/src/client.ts b/src/client.ts index c5d5457..515fb63 100644 --- a/src/client.ts +++ b/src/client.ts @@ -40,6 +40,7 @@ import { type OnrampResult, APIError, PaymentError, + RetiredEndpointError, } from "./types"; // NOTE: @blockrun/clawrouter is loaded lazily inside smartChat() (see below), // not imported at module top level. It is the model-routing engine and is only @@ -1507,22 +1508,43 @@ export class LLMClient { // ── PM convenience helpers (Predexon v2) ───────────────────────────────── // Thin wrappers over pm() / pmQuery() for the most common v2 endpoints. - /** List canonical cross-venue markets (Predexon v2). Tier 1 ($0.001/call). - * Filter with venue, status, category, league, event_id, pagination_key. */ - async pmMarkets(params?: Record): Promise> { - return this.pm("markets", params); + /** RETIRED — `/v1/pm/markets` no longer exists. + * + * Predexon sunset market matching on 2026-07-20 and the whole canonical layer + * went with it, so this path returns 410 upstream. Use + * `pm("markets/search", { q })` for cross-venue lookups. + * + * Kept as a throwing stub rather than deleted so upgrading does not break + * property access; it throws before any network I/O. + * + * @throws {RetiredEndpointError} always. */ + async pmMarkets(_params?: Record): Promise> { + throw new RetiredEndpointError( + "/v1/pm/markets was sunset by Predexon on 2026-07-20 (upstream 410). " + + 'Use pm("markets/search", { q }) for cross-venue lookups.', + ); } - /** List venue-native executable listings flattened across canonical markets - * (Predexon v2). Tier 1 ($0.001/call). */ - async pmListings(params?: Record): Promise> { - return this.pm("markets/listings", params); + /** RETIRED — `/v1/pm/markets/listings` no longer exists (410, sunset + * 2026-07-20 with market matching). Use `pm("markets/search", { q })`. + * + * @throws {RetiredEndpointError} always. */ + async pmListings(_params?: Record): Promise> { + throw new RetiredEndpointError( + "/v1/pm/markets/listings was sunset by Predexon on 2026-07-20 (upstream 410). " + + 'Use pm("markets/search", { q }) for cross-venue lookups.', + ); } - /** Resolve a canonical Predexon outcome ID to its market context and venue - * listings. Tier 1 ($0.001/call). */ - async pmOutcome(predexonId: string): Promise> { - return this.pm(`outcomes/${predexonId}`); + /** RETIRED — `/v1/pm/outcomes/{predexonId}` no longer exists (410, sunset + * 2026-07-20 with market matching). Use `pm("markets/search", { q })`. + * + * @throws {RetiredEndpointError} always. */ + async pmOutcome(_predexonId: string): Promise> { + throw new RetiredEndpointError( + "/v1/pm/outcomes/{predexon_id} was sunset by Predexon on 2026-07-20 (upstream 410). " + + 'Use pm("markets/search", { q }) for cross-venue lookups.', + ); } /** Polymarket markets with cursor-based keyset pagination (use pagination_key). @@ -1537,13 +1559,19 @@ export class LLMClient { return this.pm("polymarket/events/keyset", params); } - /** List available sports categories. Tier 1 ($0.001/call). */ + /** List available sports categories. Tier 1 ($0.001/call). + * + * NOTE: upstream returns 500 for every `sports/*` path as of 2026-08-04. + * The route still resolves, so this works again the moment Predexon restores + * it, but do not build on it yet. */ async pmSportsCategories(): Promise> { return this.pm("sports/categories"); } /** List sports markets grouped by game. Filter with league, sport_type, - * status, venue. Tier 1 ($0.001/call). */ + * status, venue. Tier 1 ($0.001/call). + * + * NOTE: upstream returns 500 for every `sports/*` path as of 2026-08-04. */ async pmSportsMarkets(params?: Record): Promise> { return this.pm("sports/markets", params); } diff --git a/src/index.ts b/src/index.ts index 5e40217..353dae2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -160,6 +160,7 @@ export { // Error classes BlockrunError, PaymentError, + RetiredEndpointError, APIError, } from "./types"; export { diff --git a/src/types.ts b/src/types.ts index 663fe51..ca2b1b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1109,6 +1109,20 @@ export class BlockrunError extends Error { } } +/** + * Thrown by a helper whose upstream endpoint no longer exists. + * + * Kept as a throwing method rather than deleted so upgrading does not break + * imports or property access — the failure is explicit and immediate instead of + * a round trip that returns 410/404. + */ +export class RetiredEndpointError extends BlockrunError { + constructor(message: string) { + super(message); + this.name = "RetiredEndpointError"; + } +} + export class PaymentError extends BlockrunError { constructor(message: string) { super(message); diff --git a/test/unit/retired-endpoints.test.ts b/test/unit/retired-endpoints.test.ts new file mode 100644 index 0000000..9bee908 --- /dev/null +++ b/test/unit/retired-endpoints.test.ts @@ -0,0 +1,41 @@ +// Helpers for endpoints Predexon retired must fail fast, not silently 410. +// +// Probed upstream 2026-08-04 (3 runs each): /v1/pm/markets, +// /v1/pm/markets/listings and /v1/pm/outcomes/{id} all return +// 410 "This endpoint has been sunset as of 2026-07-20. Market matching is +// discontinued." +// The helpers are kept rather than deleted so upgrading does not break property +// access; this pins that they throw before any network I/O. +import { describe, it, expect } from "vitest"; +import { LLMClient, RetiredEndpointError } from "../../src/index"; + +// No wallet, no network — if a helper reached fetch() these would hang or throw +// something other than RetiredEndpointError. +const client = Object.create(LLMClient.prototype) as LLMClient; + +describe("retired Predexon helpers", () => { + it("pmMarkets throws with the sunset date", async () => { + await expect(client.pmMarkets()).rejects.toThrow(RetiredEndpointError); + await expect(client.pmMarkets()).rejects.toThrow(/2026-07-20/); + }); + + it("pmListings throws with the sunset date", async () => { + await expect(client.pmListings()).rejects.toThrow(RetiredEndpointError); + await expect(client.pmListings()).rejects.toThrow(/2026-07-20/); + }); + + it("pmOutcome throws with the sunset date", async () => { + await expect(client.pmOutcome("PXM-12345")).rejects.toThrow(RetiredEndpointError); + await expect(client.pmOutcome("PXM-12345")).rejects.toThrow(/2026-07-20/); + }); + + it("points at the surviving replacement", async () => { + await expect(client.pmMarkets()).rejects.toThrow(/markets\/search/); + }); + + it("is a BlockrunError, so existing catch blocks still catch it", async () => { + const err = await client.pmMarkets().catch((e) => e); + expect(err.name).toBe("RetiredEndpointError"); + expect(err).toBeInstanceOf(Error); + }); +});