pmxt-core 2.50.16 → 2.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/exchanges/hunch/api.d.ts +736 -0
  2. package/dist/exchanges/hunch/api.js +820 -0
  3. package/dist/exchanges/hunch/auth.d.ts +36 -0
  4. package/dist/exchanges/hunch/auth.js +63 -0
  5. package/dist/exchanges/hunch/errors.d.ts +26 -0
  6. package/dist/exchanges/hunch/errors.js +94 -0
  7. package/dist/exchanges/hunch/fetcher.d.ts +204 -0
  8. package/dist/exchanges/hunch/fetcher.js +130 -0
  9. package/dist/exchanges/hunch/index.d.ts +51 -0
  10. package/dist/exchanges/hunch/index.js +330 -0
  11. package/dist/exchanges/hunch/normalizer.d.ts +56 -0
  12. package/dist/exchanges/hunch/normalizer.js +180 -0
  13. package/dist/exchanges/hunch/price.d.ts +5 -0
  14. package/dist/exchanges/hunch/price.js +12 -0
  15. package/dist/exchanges/hunch/utils.d.ts +79 -0
  16. package/dist/exchanges/hunch/utils.js +182 -0
  17. package/dist/exchanges/hunch/websocket.d.ts +25 -0
  18. package/dist/exchanges/hunch/websocket.js +137 -0
  19. package/dist/exchanges/hyperliquid/auth.js +15 -2
  20. package/dist/exchanges/hyperliquid/fetcher.d.ts +26 -1
  21. package/dist/exchanges/hyperliquid/fetcher.js +66 -4
  22. package/dist/exchanges/hyperliquid/index.d.ts +4 -1
  23. package/dist/exchanges/hyperliquid/index.js +84 -20
  24. package/dist/exchanges/hyperliquid/normalizer.d.ts +4 -3
  25. package/dist/exchanges/hyperliquid/normalizer.js +69 -11
  26. package/dist/exchanges/hyperliquid/utils.d.ts +7 -1
  27. package/dist/exchanges/hyperliquid/utils.js +16 -4
  28. package/dist/exchanges/kalshi/api.d.ts +1 -1
  29. package/dist/exchanges/kalshi/api.js +1 -1
  30. package/dist/exchanges/limitless/api.d.ts +1 -1
  31. package/dist/exchanges/limitless/api.js +1 -1
  32. package/dist/exchanges/myriad/api.d.ts +1 -1
  33. package/dist/exchanges/myriad/api.js +1 -1
  34. package/dist/exchanges/opinion/api.d.ts +1 -1
  35. package/dist/exchanges/opinion/api.js +1 -1
  36. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  37. package/dist/exchanges/polymarket/api-clob.js +1 -1
  38. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  39. package/dist/exchanges/polymarket/api-data.js +1 -1
  40. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  41. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  42. package/dist/exchanges/probable/api.d.ts +1 -1
  43. package/dist/exchanges/probable/api.js +1 -1
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.js +5 -1
  46. package/dist/server/app.js +1 -0
  47. package/dist/server/exchange-factory.js +8 -0
  48. package/dist/server/openapi.yaml +18 -0
  49. package/dist/types.d.ts +4 -0
  50. package/package.json +3 -3
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HunchExchange = void 0;
4
+ const BaseExchange_1 = require("../../BaseExchange");
5
+ const errors_1 = require("../../errors");
6
+ const validation_1 = require("../../utils/validation");
7
+ const openapi_1 = require("../../utils/openapi");
8
+ const auth_1 = require("./auth");
9
+ const fetcher_1 = require("./fetcher");
10
+ const normalizer_1 = require("./normalizer");
11
+ const websocket_1 = require("./websocket");
12
+ const errors_2 = require("./errors");
13
+ const api_1 = require("./api");
14
+ const utils_1 = require("./utils");
15
+ const AGENT_PREFIX = '/api/agent/v1';
16
+ const SIMPLE_TIER_MAX_USD = 10;
17
+ // ---------------------------------------------------------------------------
18
+ // x402 / EIP-3009 codec — ported VERBATIM from the Hunch SDK
19
+ // (packages/hunch-agent-sdk/src/x402.ts) so the signature + X-PAYMENT encoding
20
+ // the route decodes are byte-correct. pmxt-core cannot import the Hunch SDK,
21
+ // so the minimal recipe is inlined here.
22
+ // ---------------------------------------------------------------------------
23
+ const TRANSFER_WITH_AUTHORIZATION_TYPES = {
24
+ TransferWithAuthorization: [
25
+ { name: 'from', type: 'address' },
26
+ { name: 'to', type: 'address' },
27
+ { name: 'value', type: 'uint256' },
28
+ { name: 'validAfter', type: 'uint256' },
29
+ { name: 'validBefore', type: 'uint256' },
30
+ { name: 'nonce', type: 'bytes32' },
31
+ ],
32
+ };
33
+ const DEFAULT_AUTH_VALID_SECONDS = 10 * 60;
34
+ const CLOCK_SKEW_SECONDS = 60;
35
+ /** Validate + extract the single Base-USDC "exact" requirement from a 402 body. */
36
+ function parseX402Challenge(body) {
37
+ const challenge = body;
38
+ const accepts = challenge?.accepts;
39
+ const first = Array.isArray(accepts) ? accepts[0] : undefined;
40
+ if (!first || typeof first !== 'object') {
41
+ throw new errors_1.InvalidOrder('Hunch 402 challenge did not advertise a payment requirement.', 'Hunch');
42
+ }
43
+ if (first.scheme !== 'exact') {
44
+ throw new errors_1.InvalidOrder(`Unsupported Hunch x402 scheme: ${String(first.scheme)}`, 'Hunch');
45
+ }
46
+ if (first.network !== 'base') {
47
+ throw new errors_1.InvalidOrder(`Unsupported Hunch x402 network: ${String(first.network)}`, 'Hunch');
48
+ }
49
+ if (!first.payTo || !first.asset || !first.extra) {
50
+ throw new errors_1.InvalidOrder('Hunch 402 challenge is missing payTo / asset / domain extra.', 'Hunch');
51
+ }
52
+ return first;
53
+ }
54
+ function randomNonce() {
55
+ const bytes = new Uint8Array(32);
56
+ globalThis.crypto.getRandomValues(bytes);
57
+ let out = '0x';
58
+ for (const b of bytes)
59
+ out += b.toString(16).padStart(2, '0');
60
+ return out;
61
+ }
62
+ function buildTransferAuthorizationTypedData(params) {
63
+ const now = params.now ?? Math.floor(Date.now() / 1000);
64
+ const validAfter = Math.max(0, Math.floor(now) - CLOCK_SKEW_SECONDS);
65
+ const validBefore = Math.floor(now) + DEFAULT_AUTH_VALID_SECONDS;
66
+ const nonce = randomNonce();
67
+ const to = params.requirements.payTo;
68
+ const value = params.requirements.maxAmountRequired;
69
+ const authorization = { from: params.from, to, value, validAfter, validBefore, nonce };
70
+ const typedData = {
71
+ domain: {
72
+ name: params.requirements.extra.name,
73
+ version: params.requirements.extra.version,
74
+ chainId: utils_1.BASE_CHAIN_ID,
75
+ verifyingContract: params.requirements.asset,
76
+ },
77
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
78
+ primaryType: 'TransferWithAuthorization',
79
+ message: {
80
+ from: params.from,
81
+ to,
82
+ value: BigInt(value),
83
+ validAfter: BigInt(validAfter),
84
+ validBefore: BigInt(validBefore),
85
+ nonce,
86
+ },
87
+ };
88
+ return { typedData, authorization };
89
+ }
90
+ /** Encode a signed authorization into the base64 `X-PAYMENT` header value. */
91
+ function encodeXPaymentHeader(signed) {
92
+ const payload = {
93
+ x402Version: 1,
94
+ scheme: 'exact',
95
+ network: 'base',
96
+ payload: {
97
+ signature: signed.signature,
98
+ authorization: {
99
+ from: signed.from,
100
+ to: signed.to,
101
+ value: signed.value,
102
+ validAfter: signed.validAfter,
103
+ validBefore: signed.validBefore,
104
+ nonce: signed.nonce,
105
+ },
106
+ },
107
+ };
108
+ return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // HunchExchange
112
+ // ---------------------------------------------------------------------------
113
+ class HunchExchange extends BaseExchange_1.PredictionMarketExchange {
114
+ capabilityOverrides = {
115
+ fetchOrderBook: 'emulated',
116
+ createOrder: true,
117
+ cancelOrder: false,
118
+ fetchOrder: false,
119
+ fetchOpenOrders: false,
120
+ fetchBalance: true,
121
+ fetchPositions: true,
122
+ fetchSeries: false,
123
+ watchOrderBook: 'emulated',
124
+ watchTrades: 'emulated',
125
+ };
126
+ auth;
127
+ fetcher;
128
+ normalizer;
129
+ hunchBaseUrl;
130
+ ws;
131
+ constructor(credentials) {
132
+ super(credentials);
133
+ this.rateLimit = 500;
134
+ this.auth = new auth_1.HunchAuth(credentials ?? {});
135
+ this.hunchBaseUrl = credentials?.baseUrl || utils_1.DEFAULT_BASE_URL;
136
+ const descriptor = (0, openapi_1.parseOpenApiSpec)(api_1.hunchApiSpec, this.hunchBaseUrl);
137
+ this.defineImplicitApi(descriptor);
138
+ const ctx = {
139
+ http: this.http,
140
+ callApi: this.callApi.bind(this),
141
+ getHeaders: () => this.getHeaders(),
142
+ };
143
+ this.fetcher = new fetcher_1.HunchFetcher(ctx, this.hunchBaseUrl);
144
+ this.normalizer = new normalizer_1.HunchNormalizer();
145
+ }
146
+ get name() {
147
+ return 'Hunch';
148
+ }
149
+ getHeaders() {
150
+ return this.auth.getHeaders();
151
+ }
152
+ sign(_method, _path, _params) {
153
+ return this.getHeaders();
154
+ }
155
+ mapImplicitApiError(error) {
156
+ throw errors_2.hunchErrorMapper.mapError(error);
157
+ }
158
+ // -- Market data ----------------------------------------------------------
159
+ async fetchMarketsImpl(params) {
160
+ const raw = await this.fetcher.fetchRawMarkets(params);
161
+ return raw.map((m) => this.normalizer.normalizeMarket(m)).filter((m) => m !== null);
162
+ }
163
+ async fetchEventsImpl(params) {
164
+ // Hunch has no series concept; honor `series` by returning [].
165
+ if (params.series !== undefined)
166
+ return [];
167
+ const raw = await this.fetcher.fetchRawEvents(params);
168
+ return raw.map((m) => this.normalizer.normalizeEvent(m)).filter((e) => e !== null);
169
+ }
170
+ async fetchOHLCV(outcomeId, params) {
171
+ if (!params.resolution) {
172
+ throw new Error('fetchOHLCV requires a resolution parameter.');
173
+ }
174
+ const { side } = (0, utils_1.parseHunchSide)(outcomeId);
175
+ const research = await this.fetcher.fetchRawOHLCV(outcomeId, params);
176
+ return this.normalizer.normalizeOHLCV(research, params, side);
177
+ }
178
+ async fetchOrderBook(outcomeId, _limit, _params) {
179
+ const research = await this.fetcher.fetchRawOrderBook(outcomeId);
180
+ return this.normalizer.normalizeOrderBook(research, outcomeId);
181
+ }
182
+ async fetchTrades(outcomeId, params) {
183
+ (0, validation_1.validateTradesLimit)(params.limit);
184
+ const rawTrades = await this.fetcher.fetchRawTrades(outcomeId, params);
185
+ return rawTrades.map((raw, i) => this.normalizer.normalizeTrade(raw, i));
186
+ }
187
+ // -- Trading (x402 money path) --------------------------------------------
188
+ async createOrder(params) {
189
+ if (params.type === 'limit') {
190
+ throw new errors_1.InvalidOrder('Hunch is parimutuel: limit orders are unsupported, market orders only.', 'Hunch');
191
+ }
192
+ if (params.side !== 'buy') {
193
+ throw new errors_1.InvalidOrder('Hunch markets are entry-only (parimutuel pool): only side "buy" is supported.', 'Hunch');
194
+ }
195
+ const { marketId, side } = (0, utils_1.parseHunchSide)(params.outcomeId);
196
+ if (!marketId || !side) {
197
+ throw new errors_1.ValidationError(`Invalid Hunch outcomeId "${params.outcomeId}". Expected "{marketId}:{side}".`, 'outcomeId', 'Hunch');
198
+ }
199
+ const walletAddress = this.auth.requireWalletAddress('createOrder');
200
+ const account = this.auth.getAccount(); // throws if no privateKey to sign
201
+ const sizeUsd = params.amount;
202
+ const body = {
203
+ marketId,
204
+ side,
205
+ sizeUsd,
206
+ idemKey: this.buildIdemKey(marketId, side, walletAddress),
207
+ walletAddress,
208
+ simulate: false,
209
+ };
210
+ // Above the simple tier ($10) Hunch requires a price-locked quote.
211
+ if (sizeUsd > SIMPLE_TIER_MAX_USD) {
212
+ const quote = await this.fetcher.fetchRawQuote(marketId, side, sizeUsd, walletAddress);
213
+ body.quoteId = quote.quoteId;
214
+ body.minSharesOut = quote.suggestedMinSharesOut;
215
+ }
216
+ // x402 loop: serialize ONCE so the paid retry recomputes the same
217
+ // intentHash the 402 advertised (matches the Hunch SDK's `bet()`).
218
+ const serialized = JSON.stringify(body);
219
+ const tradeUrl = `${this.hunchBaseUrl}${AGENT_PREFIX}/trade`;
220
+ let res = await this.postTrade(tradeUrl, serialized);
221
+ if (res.status === 402) {
222
+ const requirements = parseX402Challenge(res.data);
223
+ const { typedData, authorization } = buildTransferAuthorizationTypedData({
224
+ from: walletAddress,
225
+ requirements,
226
+ });
227
+ const signature = (await account.signTypedData(typedData));
228
+ const paymentHeader = encodeXPaymentHeader({ ...authorization, signature });
229
+ res = await this.postTrade(tradeUrl, serialized, paymentHeader);
230
+ }
231
+ if (res.status < 200 || res.status >= 300) {
232
+ throw errors_2.hunchErrorMapper.mapError({
233
+ response: { status: res.status, data: res.data },
234
+ isAxiosError: true,
235
+ });
236
+ }
237
+ const receipt = res.data?.receipt ?? res.data;
238
+ return this.receiptToOrder(receipt, params);
239
+ }
240
+ async cancelOrder(_orderId) {
241
+ throw new errors_1.InvalidOrder('cancelOrder() is not supported by Hunch (parimutuel pool — bets cannot be cancelled).', 'Hunch');
242
+ }
243
+ async fetchOrder(_orderId) {
244
+ throw new errors_1.InvalidOrder('fetchOrder() is not supported by Hunch (parimutuel; use fetchPositions / getProof).', 'Hunch');
245
+ }
246
+ async fetchOpenOrders(_marketId) {
247
+ return []; // Parimutuel: no resting orders.
248
+ }
249
+ async fetchPositions() {
250
+ const walletAddress = this.auth.requireWalletAddress('fetchPositions');
251
+ const raw = await this.fetcher.fetchRawPositions(walletAddress);
252
+ return raw.map((p) => this.normalizer.normalizePosition(p));
253
+ }
254
+ async fetchBalance() {
255
+ const walletAddress = this.auth.requireWalletAddress('fetchBalance');
256
+ const raw = await this.fetcher.fetchRawBalance(walletAddress);
257
+ return this.normalizer.normalizeBalance(raw);
258
+ }
259
+ // -- Real-time (poll-based emulation) -------------------------------------
260
+ async watchOrderBook(outcomeId, _limit, _params = {}) {
261
+ if (!this.ws)
262
+ this.ws = this.makeWs();
263
+ return this.ws.watchOrderBook(outcomeId);
264
+ }
265
+ async watchTrades(outcomeId, _address, _since, _limit) {
266
+ if (!this.ws)
267
+ this.ws = this.makeWs();
268
+ return this.ws.watchTrades(outcomeId);
269
+ }
270
+ async close() {
271
+ if (this.ws) {
272
+ await this.ws.close();
273
+ this.ws = undefined;
274
+ }
275
+ }
276
+ // -- helpers --------------------------------------------------------------
277
+ makeWs() {
278
+ return new websocket_1.HunchWebSocket((id) => this.fetchOrderBook(id), (id, limit) => this.fetchTrades(id, { limit }));
279
+ }
280
+ /**
281
+ * POST the trade body via the axios client, NOT throwing on non-2xx so the
282
+ * 402 challenge can be read (validateStatus: always-true). Mirrors the SDK's
283
+ * fetch-based `post()` that inspects `res.status === 402`.
284
+ */
285
+ async postTrade(url, body, paymentHeader) {
286
+ return this.http.request({
287
+ url,
288
+ method: 'POST',
289
+ data: body,
290
+ headers: {
291
+ 'Content-Type': 'application/json',
292
+ ...(paymentHeader ? { 'X-PAYMENT': paymentHeader } : {}),
293
+ },
294
+ transformRequest: [(d) => d], // body is already a JSON string
295
+ validateStatus: () => true,
296
+ });
297
+ }
298
+ /** Deterministic-but-unique idempotency key (≥8 chars per the schema). */
299
+ buildIdemKey(marketId, side, wallet) {
300
+ const bytes = new Uint8Array(8);
301
+ globalThis.crypto.getRandomValues(bytes);
302
+ let suffix = '';
303
+ for (const b of bytes)
304
+ suffix += b.toString(16).padStart(2, '0');
305
+ return `pmxt-${wallet.slice(2, 10)}-${marketId}-${side}-${suffix}`.slice(0, 128);
306
+ }
307
+ receiptToOrder(receipt, params) {
308
+ const shares = Number(receipt?.position?.shares ?? 0);
309
+ const priceCents = Number(receipt?.position?.avgPriceCents ?? 0);
310
+ const sizeUsd = Number(receipt?.sizeUsd ?? params.amount);
311
+ const ts = receipt?.recordedAt ? Date.parse(receipt.recordedAt) : Date.now();
312
+ return {
313
+ id: String(receipt?.tradeId ?? `hunch-${Date.now()}`),
314
+ marketId: params.marketId,
315
+ outcomeId: params.outcomeId,
316
+ side: 'buy',
317
+ type: 'market',
318
+ price: priceCents / 100,
319
+ amount: sizeUsd,
320
+ filled: sizeUsd,
321
+ filledShares: shares,
322
+ remaining: 0,
323
+ status: 'filled',
324
+ timestamp: Number.isFinite(ts) ? ts : Date.now(),
325
+ txHash: receipt?.txHash ?? null,
326
+ chain: 'base',
327
+ };
328
+ }
329
+ }
330
+ exports.HunchExchange = HunchExchange;
@@ -0,0 +1,56 @@
1
+ import { OHLCVParams } from '../../BaseExchange';
2
+ import { UnifiedMarket, UnifiedEvent, PriceCandle, OrderBook, Trade, Position, Balance } from '../../types';
3
+ import { IExchangeNormalizer } from '../interfaces';
4
+ import { HunchRawMarket, HunchRawResearch, HunchRawOddsHistoryPoint, HunchRawPosition, HunchRawReadiness, HunchRawQuote } from './fetcher';
5
+ /**
6
+ * HunchNormalizer — pure venue→unified mappers. No I/O. Hunch is PARIMUTUEL:
7
+ * prices are implied odds (cents → 0..1), there is no real CLOB; the order book
8
+ * is emulated as a single level at the implied price with the pool as depth.
9
+ */
10
+ export declare class HunchNormalizer implements IExchangeNormalizer<HunchRawMarket, HunchRawMarket> {
11
+ /**
12
+ * Normalize a market ref. `odds`/`ladder` (from a single-market quote or
13
+ * research read) price the outcomes live; the bare list path passes neither,
14
+ * so list outcomes carry price 0 (live odds ride on the detail reads —
15
+ * mirrors Myriad's "static on list, live on detail" model).
16
+ */
17
+ normalizeMarket(raw: HunchRawMarket, odds?: {
18
+ yesPriceCents: number | null;
19
+ noPriceCents: number | null;
20
+ }, ladder?: HunchRawResearch['ladder']): UnifiedMarket | null;
21
+ /**
22
+ * Hunch has no Event tier. When the event API is used, each market is wrapped
23
+ * as a single-market event so the cross-venue ingest pipeline still works.
24
+ */
25
+ normalizeEvent(raw: HunchRawMarket): UnifiedEvent | null;
26
+ /**
27
+ * Map a research payload's oddsHistory to candles. Each fill is a flat
28
+ * candle at its realized YES probability (Myriad's flat-candle approach).
29
+ * For an N-way outcome, points whose `outcomeKey` matches are used; binary
30
+ * markets ignore `outcomeId` and use the YES trajectory.
31
+ */
32
+ normalizeOHLCV(raw: HunchRawResearch, params: OHLCVParams, outcomeKey?: string): PriceCandle[];
33
+ /**
34
+ * Emulate a single-level order book at the market's implied price, with the
35
+ * pool size (or virtual liquidity) as depth. Reuses research → odds / ladder
36
+ * / stats. For an N-way market, the level reflects the requested rung's
37
+ * implied price; binary uses the YES (or NO) implied price.
38
+ */
39
+ normalizeOrderBook(raw: HunchRawResearch, outcomeId: string): OrderBook;
40
+ /** Map a research oddsHistory point to a public trade. */
41
+ normalizeTrade(raw: HunchRawOddsHistoryPoint, index: number): Trade;
42
+ /** Map an AgentPosition → unified Position (shares, entry/now cents → 0..1). */
43
+ normalizePosition(raw: HunchRawPosition): Position;
44
+ /** Map readiness → a single USDC balance entry. */
45
+ normalizeBalance(raw: HunchRawReadiness): Balance[];
46
+ /**
47
+ * Quote → a single emulated order-book level (alternative to research when a
48
+ * size-specific implied price is wanted). Not wired into fetchOrderBook by
49
+ * default; kept for callers that have a quote in hand.
50
+ */
51
+ normalizeQuoteOrderBook(quote: HunchRawQuote): OrderBook;
52
+ private sideOf;
53
+ /** Normalize a stored position `side` to a stable outcome-id token. */
54
+ private normalizeSideKey;
55
+ private parseTs;
56
+ }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HunchNormalizer = void 0;
4
+ const metadata_1 = require("../../utils/metadata");
5
+ const utils_1 = require("./utils");
6
+ const price_1 = require("./price");
7
+ const HUNCH_PROMOTED_EVENT_KEYS = ['id', 'slug', 'question', 'markets'];
8
+ /**
9
+ * HunchNormalizer — pure venue→unified mappers. No I/O. Hunch is PARIMUTUEL:
10
+ * prices are implied odds (cents → 0..1), there is no real CLOB; the order book
11
+ * is emulated as a single level at the implied price with the pool as depth.
12
+ */
13
+ class HunchNormalizer {
14
+ /**
15
+ * Normalize a market ref. `odds`/`ladder` (from a single-market quote or
16
+ * research read) price the outcomes live; the bare list path passes neither,
17
+ * so list outcomes carry price 0 (live odds ride on the detail reads —
18
+ * mirrors Myriad's "static on list, live on detail" model).
19
+ */
20
+ normalizeMarket(raw, odds, ladder) {
21
+ return (0, utils_1.mapHunchMarketToUnified)(raw, odds, ladder ?? null);
22
+ }
23
+ /**
24
+ * Hunch has no Event tier. When the event API is used, each market is wrapped
25
+ * as a single-market event so the cross-venue ingest pipeline still works.
26
+ */
27
+ normalizeEvent(raw) {
28
+ const um = this.normalizeMarket(raw);
29
+ if (!um)
30
+ return null;
31
+ return {
32
+ id: raw.id,
33
+ title: raw.question || raw.shortTitle || '',
34
+ description: raw.summary || '',
35
+ slug: raw.slug || raw.id,
36
+ markets: [um],
37
+ volume24h: 0,
38
+ volume: um.volume,
39
+ url: raw.links?.app || 'https://www.playhunch.xyz',
40
+ category: raw.category,
41
+ tags: raw.tokenSymbol ? [raw.tokenSymbol] : [],
42
+ sourceMetadata: (0, metadata_1.buildSourceMetadata)(raw, HUNCH_PROMOTED_EVENT_KEYS),
43
+ };
44
+ }
45
+ /**
46
+ * Map a research payload's oddsHistory to candles. Each fill is a flat
47
+ * candle at its realized YES probability (Myriad's flat-candle approach).
48
+ * For an N-way outcome, points whose `outcomeKey` matches are used; binary
49
+ * markets ignore `outcomeId` and use the YES trajectory.
50
+ */
51
+ normalizeOHLCV(raw, params, outcomeKey) {
52
+ const history = raw?.oddsHistory ?? [];
53
+ let points = history;
54
+ if (outcomeKey && outcomeKey !== 'yes' && outcomeKey !== 'no') {
55
+ const matched = history.filter((p) => p.outcomeKey === outcomeKey);
56
+ if (matched.length > 0)
57
+ points = matched;
58
+ }
59
+ const candles = points
60
+ .filter((p) => typeof p.yesPct === 'number')
61
+ .map((p) => {
62
+ const prob = p.yesPct / 100;
63
+ return {
64
+ timestamp: this.parseTs(p.at),
65
+ open: prob,
66
+ high: prob,
67
+ low: prob,
68
+ close: prob,
69
+ volume: p.sizeUsd,
70
+ };
71
+ });
72
+ if (params.limit && candles.length > params.limit) {
73
+ return candles.slice(-params.limit);
74
+ }
75
+ return candles;
76
+ }
77
+ /**
78
+ * Emulate a single-level order book at the market's implied price, with the
79
+ * pool size (or virtual liquidity) as depth. Reuses research → odds / ladder
80
+ * / stats. For an N-way market, the level reflects the requested rung's
81
+ * implied price; binary uses the YES (or NO) implied price.
82
+ */
83
+ normalizeOrderBook(raw, outcomeId) {
84
+ const side = this.sideOf(outcomeId);
85
+ let price = 0;
86
+ if (raw?.ladder && raw.ladder.outcomes.length > 0 && side !== 'yes' && side !== 'no') {
87
+ const rung = raw.ladder.outcomes.find((o) => o.key === side);
88
+ price = rung ? (0, price_1.resolveHunchPrice)(rung.impliedPct) : 0;
89
+ }
90
+ else {
91
+ const yesC = raw?.odds?.yesPriceCents ?? null;
92
+ const noC = raw?.odds?.noPriceCents ?? null;
93
+ price = side === 'no' ? (0, price_1.resolveHunchPrice)(noC) : (0, price_1.resolveHunchPrice)(yesC);
94
+ }
95
+ const pool = raw?.stats?.totalPoolUsd ?? 0;
96
+ const liquidity = Number(raw?.market?.virtualLiquidityUsd ?? 0);
97
+ const size = pool > 0 ? pool : liquidity > 0 ? liquidity : 1;
98
+ return {
99
+ bids: [{ price, size }],
100
+ asks: [{ price, size }],
101
+ timestamp: Date.now(),
102
+ lastTradePrice: price || undefined,
103
+ };
104
+ }
105
+ /** Map a research oddsHistory point to a public trade. */
106
+ normalizeTrade(raw, index) {
107
+ const prob = typeof raw.yesPct === 'number' ? raw.yesPct / 100 : 0;
108
+ return {
109
+ id: `${raw.at}-${index}`,
110
+ timestamp: this.parseTs(raw.at),
111
+ price: prob,
112
+ amount: Number(raw.sizeUsd || 0),
113
+ // Hunch oddsHistory is a one-sided fill tape (every entry is a buy
114
+ // into a side); direction isn't carried, so 'buy' from the taker view.
115
+ side: 'buy',
116
+ };
117
+ }
118
+ /** Map an AgentPosition → unified Position (shares, entry/now cents → 0..1). */
119
+ normalizePosition(raw) {
120
+ const side = this.normalizeSideKey(raw.side);
121
+ return {
122
+ marketId: raw.marketId,
123
+ outcomeId: `${raw.marketId}:${side}`,
124
+ outcomeLabel: raw.outcomeLabel || side,
125
+ size: Number(raw.shares || 0),
126
+ entryPrice: (0, price_1.resolveHunchPrice)(raw.avgEntryCents),
127
+ currentPrice: (0, price_1.resolveHunchPrice)(raw.currentCents),
128
+ unrealizedPnL: Number(raw.pnlUsd || 0),
129
+ txHash: null,
130
+ chain: 'base',
131
+ };
132
+ }
133
+ /** Map readiness → a single USDC balance entry. */
134
+ normalizeBalance(raw) {
135
+ const total = typeof raw?.usdcBalanceUsd === 'number' ? raw.usdcBalanceUsd : 0;
136
+ return [
137
+ {
138
+ currency: 'USDC',
139
+ total,
140
+ available: total,
141
+ locked: 0,
142
+ },
143
+ ];
144
+ }
145
+ /**
146
+ * Quote → a single emulated order-book level (alternative to research when a
147
+ * size-specific implied price is wanted). Not wired into fetchOrderBook by
148
+ * default; kept for callers that have a quote in hand.
149
+ */
150
+ normalizeQuoteOrderBook(quote) {
151
+ const price = (0, price_1.resolveHunchPrice)(quote.priceCents);
152
+ const pool = quote.stats?.totalPoolUsd ?? 0;
153
+ const size = pool > 0 ? pool : 1;
154
+ return {
155
+ bids: [{ price, size }],
156
+ asks: [{ price, size }],
157
+ timestamp: Date.now(),
158
+ lastTradePrice: price || undefined,
159
+ };
160
+ }
161
+ // -- helpers --------------------------------------------------------------
162
+ sideOf(outcomeId) {
163
+ const idx = outcomeId.lastIndexOf(':');
164
+ return idx >= 0 ? outcomeId.slice(idx + 1) : outcomeId;
165
+ }
166
+ /** Normalize a stored position `side` to a stable outcome-id token. */
167
+ normalizeSideKey(side) {
168
+ const s = (side || '').toLowerCase();
169
+ if (s === 'yes' || s === 'no' || s === 'up' || s === 'down')
170
+ return s;
171
+ return side; // bucket key (e.g. le-330m) — keep verbatim
172
+ }
173
+ parseTs(at) {
174
+ if (!at)
175
+ return Date.now();
176
+ const ms = Date.parse(at);
177
+ return Number.isFinite(ms) ? ms : Date.now();
178
+ }
179
+ }
180
+ exports.HunchNormalizer = HunchNormalizer;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Resolve a probability (0..1) from a Hunch cents value (0..100).
3
+ * Hunch prices are integer cents on the parimutuel implied-odds scale.
4
+ */
5
+ export declare function resolveHunchPrice(cents: number | null | undefined): number;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveHunchPrice = resolveHunchPrice;
4
+ /**
5
+ * Resolve a probability (0..1) from a Hunch cents value (0..100).
6
+ * Hunch prices are integer cents on the parimutuel implied-odds scale.
7
+ */
8
+ function resolveHunchPrice(cents) {
9
+ if (typeof cents !== 'number' || !Number.isFinite(cents))
10
+ return 0;
11
+ return cents / 100;
12
+ }
@@ -0,0 +1,79 @@
1
+ import { UnifiedMarket } from '../../types';
2
+ import { HunchRawMarket } from './fetcher';
3
+ /**
4
+ * Canonical Hunch agent-platform base URL. Reads are keyless (CORS `*`);
5
+ * the trade route settles real Base USDC via x402 / EIP-3009.
6
+ */
7
+ export declare const DEFAULT_BASE_URL = "https://www.playhunch.xyz";
8
+ /** Base mainnet — the ONLY settlement chain on Hunch's agent rail. */
9
+ export declare const BASE_CHAIN_ID = 8453;
10
+ /** USDC on Base (the EIP-3009 `transferWithAuthorization` asset). */
11
+ export declare const BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
12
+ /**
13
+ * Raw Hunch fields already promoted to first-class Unified columns — excluded
14
+ * from `sourceMetadata` so we capture only what the unified shape would drop
15
+ * (e.g. headline, deadlineLabel, feeRecipientLabel, defaultTicketUsd, links).
16
+ */
17
+ export declare const HUNCH_PROMOTED_MARKET_KEYS: readonly ["id", "slug", "question", "summary", "category", "tokenSymbol", "chainId", "deadlineAt", "status", "feeBps", "virtualLiquidityUsd", "volumeUsd", "targetMarketCapUsd", "outcomes"];
18
+ /**
19
+ * Map a Hunch status (open / closed / resolved) to the pmxt unified lifecycle
20
+ * vocabulary (active / inactive / closed). Unknown → 'active' (Hunch only lists
21
+ * `open` markets when `status=open`, so this is defensive).
22
+ */
23
+ export declare function mapHunchStatus(status: string | undefined): 'active' | 'inactive' | 'closed';
24
+ /**
25
+ * Translate the pmxt unified `status` filter into the Hunch `status` query
26
+ * value. Hunch only accepts `open` | `all`; 'inactive'/'closed' have no live
27
+ * listing, so they fold to `all` (the caller filters client-side afterwards).
28
+ */
29
+ export declare function mapStatusToHunch(status?: string): 'open' | 'all' | undefined;
30
+ /**
31
+ * Compose an outcome id that round-trips back to a Hunch trade `side`.
32
+ *
33
+ * Encoding: `${marketId}:${side}` where `side` is `yes` | `no` for binary
34
+ * markets, or the parimutuel bucket `key` (e.g. `le-330m`, `up`, `down`,
35
+ * a date-window key) for N-way markets. `parseHunchSide()` reverses it.
36
+ */
37
+ export declare function buildOutcomeId(marketId: string, side: string): string;
38
+ /**
39
+ * Reverse {@link buildOutcomeId}: pull the Hunch `side` token back out of a
40
+ * unified `outcomeId`. The market id itself may contain a `:` is NOT a concern —
41
+ * Hunch ids are slug-shaped (`[a-z0-9-]`), so the FIRST colon never appears
42
+ * inside an id; we split on the LAST colon to be safe regardless.
43
+ */
44
+ export declare function parseHunchSide(outcomeId: string): {
45
+ marketId: string;
46
+ side: string;
47
+ };
48
+ /**
49
+ * Shared market normalizer used by both the live fetch path and the (rare)
50
+ * direct-mapping helper. Pulls a Hunch market ref into a {@link UnifiedMarket}.
51
+ *
52
+ * - Binary markets (`outcomes == null`) get YES/NO outcomes priced from `odds`
53
+ * when supplied (research/quote-derived), else flat 0 (the list endpoint
54
+ * does not carry live odds — they ride on the quote/research single-market
55
+ * reads, matching Myriad's "static on list, live on detail" model).
56
+ * - N-way markets expand `outcomes[]` into one {@link MarketOutcome} per rung,
57
+ * carrying `impliedPct/100` as the price when a live ladder is supplied.
58
+ *
59
+ * `outcomeId` round-trips to a Hunch `side` (see {@link buildOutcomeId}).
60
+ */
61
+ export declare function mapHunchMarketToUnified(raw: HunchRawMarket, odds?: {
62
+ yesPriceCents: number | null;
63
+ noPriceCents: number | null;
64
+ }, ladder?: {
65
+ outcomes: HunchLadderOutcome[];
66
+ } | null): UnifiedMarket | null;
67
+ /** Minimal live-ladder rung shape (mirrors AgentLadderOutcome) used for pricing. */
68
+ export interface HunchLadderOutcome {
69
+ key: string;
70
+ label: string;
71
+ shortLabel: string;
72
+ lowerUsd: number | null;
73
+ upperUsd: number | null;
74
+ startAt?: string | null;
75
+ endAt?: string | null;
76
+ impliedPct: number;
77
+ backedUsd: number;
78
+ isCurrent: boolean;
79
+ }