@predimarkets/sdk 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Predimarkets
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # @predimarkets/sdk
2
+
3
+ Official TypeScript client for the [Predimarkets](https://predimarkets.com) API — matching, oracle resolution and settlement for prediction markets.
4
+
5
+ Zero runtime dependencies. Runs on Node 18+, Bun, Deno and Cloudflare Workers.
6
+
7
+ ```bash
8
+ npm install @predimarkets/sdk
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { Predimarkets } from '@predimarkets/sdk';
15
+
16
+ const client = new Predimarkets({ apiKey: process.env.PREDIMARKETS_API_KEY! });
17
+
18
+ const { markets } = await client.markets.list({ status: 'open' });
19
+ const market = markets[0];
20
+
21
+ // Price the bet before you place it.
22
+ const quote = await client.parimutuel.quote({
23
+ marketId: market.id,
24
+ outcomeId: market.outcomes[0].id,
25
+ stake: 100,
26
+ });
27
+
28
+ // Place it.
29
+ const order = await client.orders.create({
30
+ marketId: market.id,
31
+ outcomeId: market.outcomes[0].id,
32
+ userId: 'your-user-123',
33
+ amount: 100,
34
+ });
35
+ ```
36
+
37
+ Your API key authenticates you as the partner; `userId` is **your** id for the end
38
+ user placing the bet. Keep the key server-side — it must never reach a browser.
39
+
40
+ ## Markets
41
+
42
+ ```ts
43
+ await client.markets.list({ status: 'open', limit: 20 });
44
+ await client.markets.get('will-france-win-the-world-cup-2026'); // id or slug
45
+ await client.markets.priceHistory(market.id, { timeframe: '1D' });
46
+ await client.markets.orderBook(market.id);
47
+ ```
48
+
49
+ Search is semantic, not just keyword — queries are ranked by embedding similarity
50
+ fused with full-text, so a question finds the market that answers it even when
51
+ they share no words:
52
+
53
+ ```ts
54
+ await client.markets.search('who wants to be president');
55
+ ```
56
+
57
+ ## Orders
58
+
59
+ Parimutuel markets take an `amount` — the money the user is staking, debited and
60
+ staked 1:1, with no share price to reason about:
61
+
62
+ ```ts
63
+ await client.orders.create({
64
+ marketId, outcomeId, userId: 'user-123', amount: 100,
65
+ });
66
+ ```
67
+
68
+ Order-book markets take a `quantity`, plus a `price` for limit orders:
69
+
70
+ ```ts
71
+ await client.orders.create({
72
+ marketId, outcomeId, userId: 'user-123',
73
+ side: 'buy', type: 'limit', quantity: 1000, price: 0.65,
74
+ });
75
+ ```
76
+
77
+ ## Positions and trades
78
+
79
+ ```ts
80
+ await client.positions.list({ userId: 'user-123' });
81
+ await client.trades.list({ userId: 'user-123', limit: 50 });
82
+ ```
83
+
84
+ ## Multi-outcome markets
85
+
86
+ ```ts
87
+ const state = await client.parimutuel.categorical.state(marketId);
88
+ await client.parimutuel.categorical.quote({ marketId, outcomeId, stake: 50 });
89
+ ```
90
+
91
+ An outcome that is out of the running (knocked out, lost) comes back with
92
+ `eliminated: true` and takes no new bets — the API rejects them with
93
+ `OUTCOME_ELIMINATED`. Render it as dead rather than hiding it: its share of the
94
+ pool is real, so the remaining outcomes' percentages will not add to 100 without
95
+ it.
96
+
97
+ ## Webhooks
98
+
99
+ Predimarkets signs every webhook with HMAC-SHA256 over the raw body, in the
100
+ `x-predimarkets-signature` header.
101
+
102
+ ```ts
103
+ import { Predimarkets, SIGNATURE_HEADER } from '@predimarkets/sdk';
104
+
105
+ app.post('/webhooks/predimarkets', express.raw({ type: 'application/json' }), async (req, res) => {
106
+ const event = await client.webhooks.constructEvent({
107
+ payload: req.body.toString('utf8'), // the RAW bytes, not the parsed JSON
108
+ signature: req.headers[SIGNATURE_HEADER],
109
+ secret: process.env.PREDIMARKETS_WEBHOOK_SECRET!,
110
+ });
111
+
112
+ // constructEvent throws if the signature is wrong, so you cannot forget to check.
113
+ switch (event.event_type) {
114
+ case 'market.resolved': /* pay your users */ break;
115
+ case 'positions.refund': /* credit the refund */ break;
116
+ }
117
+ res.sendStatus(200);
118
+ });
119
+ ```
120
+
121
+ Pass the **raw** body. Re-serialising parsed JSON changes a byte somewhere (key
122
+ order, whitespace) and the signature will not match. The comparison is constant
123
+ time, so it does not leak the secret to anyone timing your responses.
124
+
125
+ ## Errors
126
+
127
+ Every non-2xx response throws a `PredimarketsError`. Branch on `code`, never on
128
+ the message:
129
+
130
+ ```ts
131
+ import { PredimarketsError } from '@predimarkets/sdk';
132
+
133
+ try {
134
+ await client.orders.create({ /* … */ });
135
+ } catch (err) {
136
+ if (err instanceof PredimarketsError) {
137
+ if (err.code === 'INSUFFICIENT_BALANCE') return topUp();
138
+ if (err.code === 'OUTCOME_ELIMINATED') return refreshMarket();
139
+ console.error(err.code, err.message, err.failureReason);
140
+ }
141
+ throw err;
142
+ }
143
+ ```
144
+
145
+ ## Anything not wrapped yet
146
+
147
+ `client.request()` reaches any endpoint directly, with auth, timeouts and error
148
+ handling already applied:
149
+
150
+ ```ts
151
+ await client.request('GET', '/markets/:id/milestone-live'.replace(':id', marketId));
152
+ ```
153
+
154
+ ## License
155
+
156
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,340 @@
1
+ "use strict";
2
+ /**
3
+ * @predimarkets/sdk — the official client for the Predimarkets API.
4
+ *
5
+ * Zero runtime dependencies: it uses the platform `fetch`, so it runs on Node 18+,
6
+ * Bun, Deno, Cloudflare Workers and the browser (server-side only — an API key
7
+ * must never reach a browser).
8
+ *
9
+ * The API speaks snake_case. This SDK speaks camelCase and translates at the
10
+ * boundary, so integrators write idiomatic TypeScript and never hand-build a
11
+ * query string.
12
+ *
13
+ * const client = new Predimarkets({ apiKey: process.env.PREDIMARKETS_API_KEY! });
14
+ * const { markets } = await client.markets.list({ status: 'open' });
15
+ * await client.orders.create({
16
+ * marketId: markets[0].id,
17
+ * outcomeId: markets[0].outcomes[0].id,
18
+ * userId: 'your-user-123',
19
+ * side: 'buy',
20
+ * amount: 100,
21
+ * });
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.Webhooks = exports.SIGNATURE_HEADER = exports.Predimarkets = exports.PredimarketsError = exports.VERSION = void 0;
25
+ exports.VERSION = '0.1.0';
26
+ const DEFAULT_BASE_URL = 'https://api.predimarkets.com';
27
+ const DEFAULT_TIMEOUT_MS = 30_000;
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+ // Errors
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ /**
32
+ * Any non-2xx response from the API.
33
+ *
34
+ * `code` is the machine-readable code the API returns (INSUFFICIENT_BALANCE,
35
+ * MARKET_PAUSED, OUTCOME_ELIMINATED, …) — branch on that, never on the message.
36
+ */
37
+ class PredimarketsError extends Error {
38
+ status;
39
+ code;
40
+ /** The API's own explanation of why the request failed, when it gives one. */
41
+ failureReason;
42
+ body;
43
+ constructor(args) {
44
+ super(args.message);
45
+ this.name = 'PredimarketsError';
46
+ this.status = args.status;
47
+ this.code = args.code;
48
+ this.failureReason = args.failureReason;
49
+ this.body = args.body;
50
+ }
51
+ }
52
+ exports.PredimarketsError = PredimarketsError;
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ // Client
55
+ // ─────────────────────────────────────────────────────────────────────────────
56
+ class Predimarkets {
57
+ markets;
58
+ orders;
59
+ positions;
60
+ trades;
61
+ parimutuel;
62
+ webhooks;
63
+ apiKey;
64
+ baseUrl;
65
+ timeoutMs;
66
+ fetchImpl;
67
+ constructor(options) {
68
+ if (!options?.apiKey) {
69
+ throw new Error('Predimarkets: apiKey is required');
70
+ }
71
+ this.apiKey = options.apiKey;
72
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
73
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
74
+ const f = options.fetch ?? globalThis.fetch;
75
+ if (!f) {
76
+ throw new Error('Predimarkets: no fetch available. Use Node 18+, or pass { fetch } explicitly.');
77
+ }
78
+ this.fetchImpl = f;
79
+ this.markets = new Markets(this);
80
+ this.orders = new Orders(this);
81
+ this.positions = new Positions(this);
82
+ this.trades = new Trades(this);
83
+ this.parimutuel = new Parimutuel(this);
84
+ this.webhooks = new Webhooks();
85
+ }
86
+ /** Escape hatch: call any endpoint the SDK doesn't wrap yet. */
87
+ async request(method, path, options = {}) {
88
+ const url = new URL(`${this.baseUrl}/v1${path.startsWith('/') ? path : `/${path}`}`);
89
+ for (const [key, value] of Object.entries(options.query ?? {})) {
90
+ if (value !== undefined && value !== null)
91
+ url.searchParams.set(key, String(value));
92
+ }
93
+ const controller = new AbortController();
94
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
95
+ let response;
96
+ try {
97
+ response = await this.fetchImpl(url.toString(), {
98
+ method,
99
+ signal: controller.signal,
100
+ headers: {
101
+ Authorization: `Bearer ${this.apiKey}`,
102
+ 'Content-Type': 'application/json',
103
+ 'User-Agent': `predimarkets-sdk/${exports.VERSION}`,
104
+ },
105
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
106
+ });
107
+ }
108
+ catch (err) {
109
+ const aborted = err instanceof Error && err.name === 'AbortError';
110
+ throw new PredimarketsError({
111
+ message: aborted
112
+ ? `Request timed out after ${this.timeoutMs}ms`
113
+ : `Network error calling Predimarkets: ${err?.message ?? err}`,
114
+ status: 0,
115
+ code: aborted ? 'TIMEOUT' : 'NETWORK_ERROR',
116
+ });
117
+ }
118
+ finally {
119
+ clearTimeout(timer);
120
+ }
121
+ const text = await response.text();
122
+ let body = undefined;
123
+ try {
124
+ body = text ? JSON.parse(text) : undefined;
125
+ }
126
+ catch {
127
+ body = text;
128
+ }
129
+ if (!response.ok) {
130
+ const error = body?.error ?? {};
131
+ throw new PredimarketsError({
132
+ message: error.message ?? `Predimarkets API returned ${response.status}`,
133
+ status: response.status,
134
+ code: error.code ?? 'UNKNOWN',
135
+ failureReason: error.failure_reason,
136
+ body,
137
+ });
138
+ }
139
+ return body;
140
+ }
141
+ }
142
+ exports.Predimarkets = Predimarkets;
143
+ // ─────────────────────────────────────────────────────────────────────────────
144
+ // Resources
145
+ // ─────────────────────────────────────────────────────────────────────────────
146
+ class Markets {
147
+ client;
148
+ constructor(client) {
149
+ this.client = client;
150
+ }
151
+ /** List markets. Accepts an id or a slug anywhere a market is named. */
152
+ list(params = {}) {
153
+ return this.client.request('GET', '/markets', {
154
+ query: {
155
+ status: params.status,
156
+ limit: params.limit,
157
+ offset: params.offset,
158
+ category: params.category,
159
+ },
160
+ });
161
+ }
162
+ /**
163
+ * Search markets by meaning, not just keyword — the API ranks with embeddings
164
+ * fused with full-text, so "who wants to be president" finds the candidacy
165
+ * markets that share no words with the query.
166
+ */
167
+ search(query, params = {}) {
168
+ return this.client.request('GET', '/markets/search', {
169
+ query: { q: query, status: params.status, limit: params.limit, offset: params.offset },
170
+ });
171
+ }
172
+ get(marketIdOrSlug) {
173
+ return this.client.request('GET', `/markets/${encodeURIComponent(marketIdOrSlug)}`);
174
+ }
175
+ priceHistory(marketIdOrSlug, params = {}) {
176
+ return this.client.request('GET', `/markets/${encodeURIComponent(marketIdOrSlug)}/price-history`, {
177
+ query: { outcome_id: params.outcomeId, timeframe: params.timeframe },
178
+ });
179
+ }
180
+ orderBook(marketIdOrSlug) {
181
+ return this.client.request('GET', `/markets/${encodeURIComponent(marketIdOrSlug)}/orderbook`);
182
+ }
183
+ }
184
+ class Orders {
185
+ client;
186
+ constructor(client) {
187
+ this.client = client;
188
+ }
189
+ /**
190
+ * Place an order.
191
+ *
192
+ * For parimutuel markets pass `amount` (the money staked). For order-book
193
+ * markets pass `quantity`, plus `price` when `type` is 'limit'.
194
+ */
195
+ create(params) {
196
+ return this.client.request('POST', '/orders', {
197
+ body: {
198
+ market_id: params.marketId,
199
+ outcome_id: params.outcomeId,
200
+ partner_user_id: params.userId,
201
+ side: params.side ?? 'buy',
202
+ order_type: params.type ?? 'market',
203
+ amount: params.amount,
204
+ quantity: params.quantity,
205
+ price: params.price,
206
+ max_spend: params.maxSpend,
207
+ },
208
+ });
209
+ }
210
+ }
211
+ class Positions {
212
+ client;
213
+ constructor(client) {
214
+ this.client = client;
215
+ }
216
+ list(params = {}) {
217
+ return this.client.request('GET', '/positions', {
218
+ query: {
219
+ partner_user_id: params.userId,
220
+ market_id: params.marketId,
221
+ status: params.status,
222
+ },
223
+ });
224
+ }
225
+ }
226
+ class Trades {
227
+ client;
228
+ constructor(client) {
229
+ this.client = client;
230
+ }
231
+ list(params = {}) {
232
+ return this.client.request('GET', '/trades', {
233
+ query: {
234
+ partner_user_id: params.userId,
235
+ market_id: params.marketId,
236
+ limit: params.limit,
237
+ offset: params.offset,
238
+ },
239
+ });
240
+ }
241
+ }
242
+ class CategoricalParimutuel {
243
+ client;
244
+ constructor(client) {
245
+ this.client = client;
246
+ }
247
+ /** Price a stake on one outcome of a multi-outcome market, before placing it. */
248
+ quote(params) {
249
+ return this.client.request('GET', '/parimutuel-categorical/quote', {
250
+ query: { market_id: params.marketId, outcome_id: params.outcomeId, stake: params.stake },
251
+ });
252
+ }
253
+ /** Live pool state: per-outcome pool, probability and multiplier. */
254
+ state(marketIdOrSlug) {
255
+ return this.client.request('GET', `/parimutuel-categorical/markets/${encodeURIComponent(marketIdOrSlug)}`);
256
+ }
257
+ }
258
+ class Parimutuel {
259
+ client;
260
+ categorical;
261
+ constructor(client) {
262
+ this.client = client;
263
+ this.categorical = new CategoricalParimutuel(client);
264
+ }
265
+ /** Price a stake before placing it: fee, locked multiplier, estimated payout. */
266
+ quote(params) {
267
+ return this.client.request('GET', '/parimutuel/quote', {
268
+ query: {
269
+ market_id: params.marketId,
270
+ outcome_id: params.outcomeId,
271
+ stake: params.stake,
272
+ side: params.side,
273
+ },
274
+ });
275
+ }
276
+ /** Live pool state for a binary parimutuel market. */
277
+ state(marketIdOrSlug) {
278
+ return this.client.request('GET', `/parimutuel/markets/${encodeURIComponent(marketIdOrSlug)}`);
279
+ }
280
+ ticketValue(marketIdOrSlug, ticketId) {
281
+ return this.client.request('GET', `/parimutuel/markets/${encodeURIComponent(marketIdOrSlug)}/tickets/${encodeURIComponent(ticketId)}/value`);
282
+ }
283
+ }
284
+ // ─────────────────────────────────────────────────────────────────────────────
285
+ // Webhooks
286
+ // ─────────────────────────────────────────────────────────────────────────────
287
+ /** Header Predimarkets signs every webhook with. */
288
+ exports.SIGNATURE_HEADER = 'x-predimarkets-signature';
289
+ class Webhooks {
290
+ /**
291
+ * Verify a webhook actually came from Predimarkets.
292
+ *
293
+ * Pass the RAW request body — the exact bytes received. Re-serialising the
294
+ * parsed JSON will change a byte somewhere (key order, whitespace) and the
295
+ * signature will not match.
296
+ *
297
+ * Compared in constant time: a plain `===` leaks the secret one byte at a time
298
+ * to anyone who can measure the response.
299
+ */
300
+ async verify(params) {
301
+ if (!params.signature || !params.secret)
302
+ return false;
303
+ const expected = await hmacSha256Hex(params.secret, params.payload);
304
+ return timingSafeEqual(expected, params.signature);
305
+ }
306
+ /**
307
+ * Verify and parse in one step. Throws if the signature is wrong, so a handler
308
+ * cannot forget to check.
309
+ */
310
+ async constructEvent(params) {
311
+ const ok = await this.verify(params);
312
+ if (!ok) {
313
+ throw new PredimarketsError({
314
+ message: 'Webhook signature verification failed',
315
+ status: 400,
316
+ code: 'INVALID_SIGNATURE',
317
+ });
318
+ }
319
+ return JSON.parse(params.payload);
320
+ }
321
+ }
322
+ exports.Webhooks = Webhooks;
323
+ /** HMAC-SHA256, hex — via WebCrypto so this works everywhere, not just Node. */
324
+ async function hmacSha256Hex(secret, payload) {
325
+ const encoder = new TextEncoder();
326
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
327
+ const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
328
+ return Array.from(new Uint8Array(sig))
329
+ .map((b) => b.toString(16).padStart(2, '0'))
330
+ .join('');
331
+ }
332
+ function timingSafeEqual(a, b) {
333
+ if (a.length !== b.length)
334
+ return false;
335
+ let diff = 0;
336
+ for (let i = 0; i < a.length; i++)
337
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
338
+ return diff === 0;
339
+ }
340
+ exports.default = Predimarkets;
@@ -0,0 +1,239 @@
1
+ /**
2
+ * @predimarkets/sdk — the official client for the Predimarkets API.
3
+ *
4
+ * Zero runtime dependencies: it uses the platform `fetch`, so it runs on Node 18+,
5
+ * Bun, Deno, Cloudflare Workers and the browser (server-side only — an API key
6
+ * must never reach a browser).
7
+ *
8
+ * The API speaks snake_case. This SDK speaks camelCase and translates at the
9
+ * boundary, so integrators write idiomatic TypeScript and never hand-build a
10
+ * query string.
11
+ *
12
+ * const client = new Predimarkets({ apiKey: process.env.PREDIMARKETS_API_KEY! });
13
+ * const { markets } = await client.markets.list({ status: 'open' });
14
+ * await client.orders.create({
15
+ * marketId: markets[0].id,
16
+ * outcomeId: markets[0].outcomes[0].id,
17
+ * userId: 'your-user-123',
18
+ * side: 'buy',
19
+ * amount: 100,
20
+ * });
21
+ */
22
+ export declare const VERSION = "0.1.0";
23
+ /**
24
+ * Any non-2xx response from the API.
25
+ *
26
+ * `code` is the machine-readable code the API returns (INSUFFICIENT_BALANCE,
27
+ * MARKET_PAUSED, OUTCOME_ELIMINATED, …) — branch on that, never on the message.
28
+ */
29
+ export declare class PredimarketsError extends Error {
30
+ readonly status: number;
31
+ readonly code: string;
32
+ /** The API's own explanation of why the request failed, when it gives one. */
33
+ readonly failureReason?: string;
34
+ readonly body: unknown;
35
+ constructor(args: {
36
+ message: string;
37
+ status: number;
38
+ code: string;
39
+ failureReason?: string;
40
+ body?: unknown;
41
+ });
42
+ }
43
+ export interface ClientOptions {
44
+ /** Your partner API key. Server-side only — never ship this to a browser. */
45
+ apiKey: string;
46
+ /** Override the API host (e.g. a staging environment). */
47
+ baseUrl?: string;
48
+ /** Abort a request after this many ms. Default 30s. */
49
+ timeoutMs?: number;
50
+ /** Supply your own fetch (for tests, or a runtime without a global one). */
51
+ fetch?: typeof fetch;
52
+ }
53
+ export type MarketStatus = 'open' | 'closed' | 'resolved' | 'voided' | 'scheduled';
54
+ export type OrderSide = 'buy' | 'sell';
55
+ export type OrderType = 'limit' | 'market';
56
+ export interface Outcome {
57
+ id: string;
58
+ label: string;
59
+ display_order: number;
60
+ current_price: number | null;
61
+ potential_return: number | null;
62
+ total_volume?: number;
63
+ /** Multi-outcome markets: this runner is out and takes no new bets. */
64
+ eliminated?: boolean;
65
+ eliminated_reason?: string | null;
66
+ }
67
+ export interface Market {
68
+ id: string;
69
+ title: string;
70
+ slug: string;
71
+ description: string | null;
72
+ image_url: string | null;
73
+ market_type: string;
74
+ source: string;
75
+ status: MarketStatus | string;
76
+ trading_ends_at: string;
77
+ resolution_at: string;
78
+ resolution_criteria?: string | null;
79
+ resolution?: string | null;
80
+ outcomes: Outcome[];
81
+ total_volume?: number;
82
+ traders?: number;
83
+ [key: string]: unknown;
84
+ }
85
+ export interface Paginated<T> {
86
+ markets?: T[];
87
+ total?: number;
88
+ limit?: number;
89
+ offset?: number;
90
+ [key: string]: unknown;
91
+ }
92
+ export interface CreateOrderParams {
93
+ marketId: string;
94
+ outcomeId: string;
95
+ /** Your own id for the end user placing the bet. */
96
+ userId: string;
97
+ side?: OrderSide;
98
+ type?: OrderType;
99
+ /**
100
+ * Parimutuel: the amount of money the user is staking. Preferred — it is
101
+ * debited and staked 1:1, with no separate share price to reason about.
102
+ */
103
+ amount?: number;
104
+ /** Order book: number of shares. */
105
+ quantity?: number;
106
+ /** Order book limit orders: price per share, 0–1. */
107
+ price?: number;
108
+ /** Hard cap on total spend including fees. Buy orders only. */
109
+ maxSpend?: number;
110
+ }
111
+ export interface QuoteParams {
112
+ marketId: string;
113
+ outcomeId: string;
114
+ stake: number;
115
+ /** Binary parimutuel only. */
116
+ side?: 'yes' | 'no';
117
+ }
118
+ export declare class Predimarkets {
119
+ readonly markets: Markets;
120
+ readonly orders: Orders;
121
+ readonly positions: Positions;
122
+ readonly trades: Trades;
123
+ readonly parimutuel: Parimutuel;
124
+ readonly webhooks: Webhooks;
125
+ private readonly apiKey;
126
+ private readonly baseUrl;
127
+ private readonly timeoutMs;
128
+ private readonly fetchImpl;
129
+ constructor(options: ClientOptions);
130
+ /** Escape hatch: call any endpoint the SDK doesn't wrap yet. */
131
+ request<T = unknown>(method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, options?: {
132
+ query?: Record<string, unknown>;
133
+ body?: unknown;
134
+ }): Promise<T>;
135
+ }
136
+ declare class Markets {
137
+ private readonly client;
138
+ constructor(client: Predimarkets);
139
+ /** List markets. Accepts an id or a slug anywhere a market is named. */
140
+ list(params?: {
141
+ status?: string;
142
+ limit?: number;
143
+ offset?: number;
144
+ category?: string;
145
+ }): Promise<Paginated<Market>>;
146
+ /**
147
+ * Search markets by meaning, not just keyword — the API ranks with embeddings
148
+ * fused with full-text, so "who wants to be president" finds the candidacy
149
+ * markets that share no words with the query.
150
+ */
151
+ search(query: string, params?: {
152
+ status?: string;
153
+ limit?: number;
154
+ offset?: number;
155
+ }): Promise<Paginated<Market>>;
156
+ get(marketIdOrSlug: string): Promise<Market>;
157
+ priceHistory(marketIdOrSlug: string, params?: {
158
+ outcomeId?: string;
159
+ timeframe?: '1H' | '1D' | '1W' | '1M' | 'ALL';
160
+ }): Promise<unknown>;
161
+ orderBook(marketIdOrSlug: string): Promise<unknown>;
162
+ }
163
+ declare class Orders {
164
+ private readonly client;
165
+ constructor(client: Predimarkets);
166
+ /**
167
+ * Place an order.
168
+ *
169
+ * For parimutuel markets pass `amount` (the money staked). For order-book
170
+ * markets pass `quantity`, plus `price` when `type` is 'limit'.
171
+ */
172
+ create(params: CreateOrderParams): Promise<unknown>;
173
+ }
174
+ declare class Positions {
175
+ private readonly client;
176
+ constructor(client: Predimarkets);
177
+ list(params?: {
178
+ userId?: string;
179
+ marketId?: string;
180
+ status?: string;
181
+ }): Promise<unknown>;
182
+ }
183
+ declare class Trades {
184
+ private readonly client;
185
+ constructor(client: Predimarkets);
186
+ list(params?: {
187
+ userId?: string;
188
+ marketId?: string;
189
+ limit?: number;
190
+ offset?: number;
191
+ }): Promise<unknown>;
192
+ }
193
+ declare class CategoricalParimutuel {
194
+ private readonly client;
195
+ constructor(client: Predimarkets);
196
+ /** Price a stake on one outcome of a multi-outcome market, before placing it. */
197
+ quote(params: QuoteParams): Promise<unknown>;
198
+ /** Live pool state: per-outcome pool, probability and multiplier. */
199
+ state(marketIdOrSlug: string): Promise<unknown>;
200
+ }
201
+ declare class Parimutuel {
202
+ private readonly client;
203
+ readonly categorical: CategoricalParimutuel;
204
+ constructor(client: Predimarkets);
205
+ /** Price a stake before placing it: fee, locked multiplier, estimated payout. */
206
+ quote(params: QuoteParams): Promise<unknown>;
207
+ /** Live pool state for a binary parimutuel market. */
208
+ state(marketIdOrSlug: string): Promise<unknown>;
209
+ ticketValue(marketIdOrSlug: string, ticketId: string): Promise<unknown>;
210
+ }
211
+ /** Header Predimarkets signs every webhook with. */
212
+ export declare const SIGNATURE_HEADER = "x-predimarkets-signature";
213
+ export declare class Webhooks {
214
+ /**
215
+ * Verify a webhook actually came from Predimarkets.
216
+ *
217
+ * Pass the RAW request body — the exact bytes received. Re-serialising the
218
+ * parsed JSON will change a byte somewhere (key order, whitespace) and the
219
+ * signature will not match.
220
+ *
221
+ * Compared in constant time: a plain `===` leaks the secret one byte at a time
222
+ * to anyone who can measure the response.
223
+ */
224
+ verify(params: {
225
+ payload: string;
226
+ signature: string | null | undefined;
227
+ secret: string;
228
+ }): Promise<boolean>;
229
+ /**
230
+ * Verify and parse in one step. Throws if the signature is wrong, so a handler
231
+ * cannot forget to check.
232
+ */
233
+ constructEvent<T = unknown>(params: {
234
+ payload: string;
235
+ signature: string | null | undefined;
236
+ secret: string;
237
+ }): Promise<T>;
238
+ }
239
+ export default Predimarkets;
package/dist/index.js ADDED
@@ -0,0 +1,334 @@
1
+ /**
2
+ * @predimarkets/sdk — the official client for the Predimarkets API.
3
+ *
4
+ * Zero runtime dependencies: it uses the platform `fetch`, so it runs on Node 18+,
5
+ * Bun, Deno, Cloudflare Workers and the browser (server-side only — an API key
6
+ * must never reach a browser).
7
+ *
8
+ * The API speaks snake_case. This SDK speaks camelCase and translates at the
9
+ * boundary, so integrators write idiomatic TypeScript and never hand-build a
10
+ * query string.
11
+ *
12
+ * const client = new Predimarkets({ apiKey: process.env.PREDIMARKETS_API_KEY! });
13
+ * const { markets } = await client.markets.list({ status: 'open' });
14
+ * await client.orders.create({
15
+ * marketId: markets[0].id,
16
+ * outcomeId: markets[0].outcomes[0].id,
17
+ * userId: 'your-user-123',
18
+ * side: 'buy',
19
+ * amount: 100,
20
+ * });
21
+ */
22
+ export const VERSION = '0.1.0';
23
+ const DEFAULT_BASE_URL = 'https://api.predimarkets.com';
24
+ const DEFAULT_TIMEOUT_MS = 30_000;
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ // Errors
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+ /**
29
+ * Any non-2xx response from the API.
30
+ *
31
+ * `code` is the machine-readable code the API returns (INSUFFICIENT_BALANCE,
32
+ * MARKET_PAUSED, OUTCOME_ELIMINATED, …) — branch on that, never on the message.
33
+ */
34
+ export class PredimarketsError extends Error {
35
+ status;
36
+ code;
37
+ /** The API's own explanation of why the request failed, when it gives one. */
38
+ failureReason;
39
+ body;
40
+ constructor(args) {
41
+ super(args.message);
42
+ this.name = 'PredimarketsError';
43
+ this.status = args.status;
44
+ this.code = args.code;
45
+ this.failureReason = args.failureReason;
46
+ this.body = args.body;
47
+ }
48
+ }
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+ // Client
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+ export class Predimarkets {
53
+ markets;
54
+ orders;
55
+ positions;
56
+ trades;
57
+ parimutuel;
58
+ webhooks;
59
+ apiKey;
60
+ baseUrl;
61
+ timeoutMs;
62
+ fetchImpl;
63
+ constructor(options) {
64
+ if (!options?.apiKey) {
65
+ throw new Error('Predimarkets: apiKey is required');
66
+ }
67
+ this.apiKey = options.apiKey;
68
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
69
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
70
+ const f = options.fetch ?? globalThis.fetch;
71
+ if (!f) {
72
+ throw new Error('Predimarkets: no fetch available. Use Node 18+, or pass { fetch } explicitly.');
73
+ }
74
+ this.fetchImpl = f;
75
+ this.markets = new Markets(this);
76
+ this.orders = new Orders(this);
77
+ this.positions = new Positions(this);
78
+ this.trades = new Trades(this);
79
+ this.parimutuel = new Parimutuel(this);
80
+ this.webhooks = new Webhooks();
81
+ }
82
+ /** Escape hatch: call any endpoint the SDK doesn't wrap yet. */
83
+ async request(method, path, options = {}) {
84
+ const url = new URL(`${this.baseUrl}/v1${path.startsWith('/') ? path : `/${path}`}`);
85
+ for (const [key, value] of Object.entries(options.query ?? {})) {
86
+ if (value !== undefined && value !== null)
87
+ url.searchParams.set(key, String(value));
88
+ }
89
+ const controller = new AbortController();
90
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
91
+ let response;
92
+ try {
93
+ response = await this.fetchImpl(url.toString(), {
94
+ method,
95
+ signal: controller.signal,
96
+ headers: {
97
+ Authorization: `Bearer ${this.apiKey}`,
98
+ 'Content-Type': 'application/json',
99
+ 'User-Agent': `predimarkets-sdk/${VERSION}`,
100
+ },
101
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
102
+ });
103
+ }
104
+ catch (err) {
105
+ const aborted = err instanceof Error && err.name === 'AbortError';
106
+ throw new PredimarketsError({
107
+ message: aborted
108
+ ? `Request timed out after ${this.timeoutMs}ms`
109
+ : `Network error calling Predimarkets: ${err?.message ?? err}`,
110
+ status: 0,
111
+ code: aborted ? 'TIMEOUT' : 'NETWORK_ERROR',
112
+ });
113
+ }
114
+ finally {
115
+ clearTimeout(timer);
116
+ }
117
+ const text = await response.text();
118
+ let body = undefined;
119
+ try {
120
+ body = text ? JSON.parse(text) : undefined;
121
+ }
122
+ catch {
123
+ body = text;
124
+ }
125
+ if (!response.ok) {
126
+ const error = body?.error ?? {};
127
+ throw new PredimarketsError({
128
+ message: error.message ?? `Predimarkets API returned ${response.status}`,
129
+ status: response.status,
130
+ code: error.code ?? 'UNKNOWN',
131
+ failureReason: error.failure_reason,
132
+ body,
133
+ });
134
+ }
135
+ return body;
136
+ }
137
+ }
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+ // Resources
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+ class Markets {
142
+ client;
143
+ constructor(client) {
144
+ this.client = client;
145
+ }
146
+ /** List markets. Accepts an id or a slug anywhere a market is named. */
147
+ list(params = {}) {
148
+ return this.client.request('GET', '/markets', {
149
+ query: {
150
+ status: params.status,
151
+ limit: params.limit,
152
+ offset: params.offset,
153
+ category: params.category,
154
+ },
155
+ });
156
+ }
157
+ /**
158
+ * Search markets by meaning, not just keyword — the API ranks with embeddings
159
+ * fused with full-text, so "who wants to be president" finds the candidacy
160
+ * markets that share no words with the query.
161
+ */
162
+ search(query, params = {}) {
163
+ return this.client.request('GET', '/markets/search', {
164
+ query: { q: query, status: params.status, limit: params.limit, offset: params.offset },
165
+ });
166
+ }
167
+ get(marketIdOrSlug) {
168
+ return this.client.request('GET', `/markets/${encodeURIComponent(marketIdOrSlug)}`);
169
+ }
170
+ priceHistory(marketIdOrSlug, params = {}) {
171
+ return this.client.request('GET', `/markets/${encodeURIComponent(marketIdOrSlug)}/price-history`, {
172
+ query: { outcome_id: params.outcomeId, timeframe: params.timeframe },
173
+ });
174
+ }
175
+ orderBook(marketIdOrSlug) {
176
+ return this.client.request('GET', `/markets/${encodeURIComponent(marketIdOrSlug)}/orderbook`);
177
+ }
178
+ }
179
+ class Orders {
180
+ client;
181
+ constructor(client) {
182
+ this.client = client;
183
+ }
184
+ /**
185
+ * Place an order.
186
+ *
187
+ * For parimutuel markets pass `amount` (the money staked). For order-book
188
+ * markets pass `quantity`, plus `price` when `type` is 'limit'.
189
+ */
190
+ create(params) {
191
+ return this.client.request('POST', '/orders', {
192
+ body: {
193
+ market_id: params.marketId,
194
+ outcome_id: params.outcomeId,
195
+ partner_user_id: params.userId,
196
+ side: params.side ?? 'buy',
197
+ order_type: params.type ?? 'market',
198
+ amount: params.amount,
199
+ quantity: params.quantity,
200
+ price: params.price,
201
+ max_spend: params.maxSpend,
202
+ },
203
+ });
204
+ }
205
+ }
206
+ class Positions {
207
+ client;
208
+ constructor(client) {
209
+ this.client = client;
210
+ }
211
+ list(params = {}) {
212
+ return this.client.request('GET', '/positions', {
213
+ query: {
214
+ partner_user_id: params.userId,
215
+ market_id: params.marketId,
216
+ status: params.status,
217
+ },
218
+ });
219
+ }
220
+ }
221
+ class Trades {
222
+ client;
223
+ constructor(client) {
224
+ this.client = client;
225
+ }
226
+ list(params = {}) {
227
+ return this.client.request('GET', '/trades', {
228
+ query: {
229
+ partner_user_id: params.userId,
230
+ market_id: params.marketId,
231
+ limit: params.limit,
232
+ offset: params.offset,
233
+ },
234
+ });
235
+ }
236
+ }
237
+ class CategoricalParimutuel {
238
+ client;
239
+ constructor(client) {
240
+ this.client = client;
241
+ }
242
+ /** Price a stake on one outcome of a multi-outcome market, before placing it. */
243
+ quote(params) {
244
+ return this.client.request('GET', '/parimutuel-categorical/quote', {
245
+ query: { market_id: params.marketId, outcome_id: params.outcomeId, stake: params.stake },
246
+ });
247
+ }
248
+ /** Live pool state: per-outcome pool, probability and multiplier. */
249
+ state(marketIdOrSlug) {
250
+ return this.client.request('GET', `/parimutuel-categorical/markets/${encodeURIComponent(marketIdOrSlug)}`);
251
+ }
252
+ }
253
+ class Parimutuel {
254
+ client;
255
+ categorical;
256
+ constructor(client) {
257
+ this.client = client;
258
+ this.categorical = new CategoricalParimutuel(client);
259
+ }
260
+ /** Price a stake before placing it: fee, locked multiplier, estimated payout. */
261
+ quote(params) {
262
+ return this.client.request('GET', '/parimutuel/quote', {
263
+ query: {
264
+ market_id: params.marketId,
265
+ outcome_id: params.outcomeId,
266
+ stake: params.stake,
267
+ side: params.side,
268
+ },
269
+ });
270
+ }
271
+ /** Live pool state for a binary parimutuel market. */
272
+ state(marketIdOrSlug) {
273
+ return this.client.request('GET', `/parimutuel/markets/${encodeURIComponent(marketIdOrSlug)}`);
274
+ }
275
+ ticketValue(marketIdOrSlug, ticketId) {
276
+ return this.client.request('GET', `/parimutuel/markets/${encodeURIComponent(marketIdOrSlug)}/tickets/${encodeURIComponent(ticketId)}/value`);
277
+ }
278
+ }
279
+ // ─────────────────────────────────────────────────────────────────────────────
280
+ // Webhooks
281
+ // ─────────────────────────────────────────────────────────────────────────────
282
+ /** Header Predimarkets signs every webhook with. */
283
+ export const SIGNATURE_HEADER = 'x-predimarkets-signature';
284
+ export class Webhooks {
285
+ /**
286
+ * Verify a webhook actually came from Predimarkets.
287
+ *
288
+ * Pass the RAW request body — the exact bytes received. Re-serialising the
289
+ * parsed JSON will change a byte somewhere (key order, whitespace) and the
290
+ * signature will not match.
291
+ *
292
+ * Compared in constant time: a plain `===` leaks the secret one byte at a time
293
+ * to anyone who can measure the response.
294
+ */
295
+ async verify(params) {
296
+ if (!params.signature || !params.secret)
297
+ return false;
298
+ const expected = await hmacSha256Hex(params.secret, params.payload);
299
+ return timingSafeEqual(expected, params.signature);
300
+ }
301
+ /**
302
+ * Verify and parse in one step. Throws if the signature is wrong, so a handler
303
+ * cannot forget to check.
304
+ */
305
+ async constructEvent(params) {
306
+ const ok = await this.verify(params);
307
+ if (!ok) {
308
+ throw new PredimarketsError({
309
+ message: 'Webhook signature verification failed',
310
+ status: 400,
311
+ code: 'INVALID_SIGNATURE',
312
+ });
313
+ }
314
+ return JSON.parse(params.payload);
315
+ }
316
+ }
317
+ /** HMAC-SHA256, hex — via WebCrypto so this works everywhere, not just Node. */
318
+ async function hmacSha256Hex(secret, payload) {
319
+ const encoder = new TextEncoder();
320
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
321
+ const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
322
+ return Array.from(new Uint8Array(sig))
323
+ .map((b) => b.toString(16).padStart(2, '0'))
324
+ .join('');
325
+ }
326
+ function timingSafeEqual(a, b) {
327
+ if (a.length !== b.length)
328
+ return false;
329
+ let diff = 0;
330
+ for (let i = 0; i < a.length; i++)
331
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
332
+ return diff === 0;
333
+ }
334
+ export default Predimarkets;
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@predimarkets/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Official TypeScript client for the Predimarkets API — matching, oracle resolution and settlement for prediction markets.",
5
+ "keywords": ["predimarkets", "prediction-markets", "parimutuel", "betting", "sdk", "api-client"],
6
+ "homepage": "https://predimarkets.com",
7
+ "license": "MIT",
8
+ "author": "Predimarkets",
9
+ "repository": { "type": "git", "url": "https://github.com/musakkka/predimarkets-sdk.git" },
10
+ "type": "module",
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "files": ["dist", "README.md", "LICENSE"],
22
+ "engines": { "node": ">=18" },
23
+ "sideEffects": false,
24
+ "scripts": {
25
+ "build": "rm -rf dist && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node scripts/rename-cjs.mjs",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.9.3",
30
+ "@types/node": "^20.19.0"
31
+ }
32
+ }