@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 +21 -0
- package/README.md +156 -0
- package/dist/index.cjs +340 -0
- package/dist/index.d.ts +239 -0
- package/dist/index.js +334 -0
- package/package.json +32 -0
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;
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|