@puul/partner-sdk 1.0.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/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # @puul/partner-sdk
2
+
3
+ Official TypeScript SDK for the **Puul Partner API** — integrate prediction markets into your platform with type safety and zero configuration.
4
+
5
+ ## Features
6
+
7
+ - 🔐 **Auto-authentication** — OAuth2 token management handled for you
8
+ - 📦 **Zero runtime dependencies** — uses native `fetch` (Node 18+)
9
+ - 🎯 **Fully typed** — complete TypeScript definitions for all API shapes
10
+ - 🪝 **Webhook helpers** — signature verification & typed event parsing
11
+ - ⚡ **Dual output** — ESM + CommonJS builds
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @puul/partner-sdk
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ import { PuulPartner } from '@puul/partner-sdk';
23
+
24
+ const puul = new PuulPartner({
25
+ clientId: 'pk_live_abc123',
26
+ clientSecret: 'sk_live_secret789',
27
+ });
28
+
29
+ // List live markets
30
+ const markets = await puul.markets.list();
31
+ console.log(markets[0].question);
32
+ // → "Will Bitcoin reach $100k by March?"
33
+
34
+ // Place a prediction
35
+ const prediction = await puul.predictions.place({
36
+ marketId: markets[0].id,
37
+ outcomeId: markets[0].outcomes[0].id,
38
+ stakeAmount: 100000, // ₦1,000 (minor units)
39
+ stakeCurrency: 'NGN',
40
+ idempotencyKey: `pred_${Date.now()}`,
41
+ });
42
+ ```
43
+
44
+ ## API Reference
45
+
46
+ ### Authentication
47
+
48
+ The SDK automatically manages OAuth2 tokens. You never need to call the auth endpoint directly.
49
+
50
+ ```typescript
51
+ const puul = new PuulPartner({
52
+ clientId: 'pk_live_abc123',
53
+ clientSecret: 'sk_live_secret789',
54
+ baseUrl: 'https://api.joinpuul.com/api/v1', // optional
55
+ timeoutMs: 30000, // optional
56
+ });
57
+ ```
58
+
59
+ ### User Linking
60
+
61
+ ```typescript
62
+ // 1. Create a link token (server-side)
63
+ const linkToken = await puul.sessions.createLinkToken({
64
+ externalUserId: 'your-user-id-123',
65
+ email: 'user@example.com', // optional
66
+ countryCode: 'NG', // optional
67
+ });
68
+
69
+ // 2. Exchange link token for a session
70
+ const session = await puul.sessions.create(linkToken.linkToken);
71
+ // session.access_token is scoped to the linked user
72
+ ```
73
+
74
+ ### Markets
75
+
76
+ ```typescript
77
+ // All live markets
78
+ const markets = await puul.markets.list();
79
+
80
+ // Filter by country
81
+ const ngMarkets = await puul.markets.list(['NG', 'GH']);
82
+ ```
83
+
84
+ ### Predictions
85
+
86
+ ```typescript
87
+ // Optional: Lock in odds with a quote (10s expiry)
88
+ const quote = await puul.predictions.createQuote({
89
+ marketId: 'market-uuid',
90
+ outcomeId: 'outcome-uuid',
91
+ stakeAmount: 50000,
92
+ stakeCurrency: 'NGN',
93
+ });
94
+
95
+ // Place prediction (with or without quote)
96
+ const prediction = await puul.predictions.place({
97
+ marketId: 'market-uuid',
98
+ outcomeId: 'outcome-uuid',
99
+ stakeAmount: 50000,
100
+ stakeCurrency: 'NGN',
101
+ idempotencyKey: 'unique-key',
102
+ quoteId: quote.quoteId, // optional
103
+ });
104
+
105
+ // Get prediction details
106
+ const details = await puul.predictions.get(prediction.id);
107
+
108
+ // List user predictions
109
+ const history = await puul.predictions.list({ status: 'OPEN', limit: 20 });
110
+ ```
111
+
112
+ ### Pending Predictions (JIT Funding)
113
+
114
+ ```typescript
115
+ // Create a pending prediction — returns bank payment details
116
+ const pending = await puul.predictions.createPending({
117
+ marketId: 'market-uuid',
118
+ outcomeId: 'outcome-uuid',
119
+ stakeAmount: 500000,
120
+ stakeCurrency: 'NGN',
121
+ userExternalId: 'your-user-id',
122
+ idempotencyKey: 'unique-key',
123
+ });
124
+ // pending.paymentReference — use this in the bank transfer
125
+
126
+ // Check status
127
+ const status = await puul.predictions.getPending(pending.id);
128
+ // status.status: 'pending' → 'funded' → 'placed'
129
+ ```
130
+
131
+ ### Wallet
132
+
133
+ ```typescript
134
+ const balance = await puul.wallet.getBalance();
135
+ const omnibus = await puul.wallet.getOmnibusBalance('USDC');
136
+
137
+ await puul.wallet.deposit({
138
+ amount: 5000, // $50.00 in minor units
139
+ currency: 'USDC',
140
+ idempotency_key: 'dep_unique_123',
141
+ });
142
+ ```
143
+
144
+ ### Webhook Verification
145
+
146
+ ```typescript
147
+ import { verifyWebhookSignature, parseWebhookEvent } from '@puul/partner-sdk';
148
+
149
+ app.post('/webhooks/puul', express.raw({ type: '*/*' }), (req, res) => {
150
+ const isValid = verifyWebhookSignature(
151
+ req.body.toString(),
152
+ req.headers['x-puul-signature'] as string,
153
+ process.env.PUUL_WEBHOOK_SECRET!,
154
+ );
155
+
156
+ if (!isValid) {
157
+ return res.status(401).send('Invalid signature');
158
+ }
159
+
160
+ const event = parseWebhookEvent(JSON.parse(req.body.toString()));
161
+
162
+ switch (event.event) {
163
+ case 'prediction.settled':
164
+ console.log('Prediction settled:', event.data);
165
+ break;
166
+ case 'deposit.confirmed':
167
+ console.log('Deposit confirmed:', event.data);
168
+ break;
169
+ }
170
+
171
+ res.status(200).send('OK');
172
+ });
173
+ ```
174
+
175
+ ## Error Handling
176
+
177
+ All API errors throw a `PuulError` with structured details:
178
+
179
+ ```typescript
180
+ import { PuulError } from '@puul/partner-sdk';
181
+
182
+ try {
183
+ await puul.predictions.place(params);
184
+ } catch (error) {
185
+ if (error instanceof PuulError) {
186
+ console.error(error.code); // 'INSUFFICIENT_BALANCE'
187
+ console.error(error.message); // 'Insufficient wallet balance'
188
+ console.error(error.statusCode); // 400
189
+ console.error(error.requestId); // 'req_abc123'
190
+ console.error(error.retryable); // false
191
+ }
192
+ }
193
+ ```
194
+
195
+ ## Supported Currencies
196
+
197
+ | Code | Currency |
198
+ |------|----------|
199
+ | `NGN` | Nigerian Naira |
200
+ | `USDC` | USD Coin |
201
+ | `USDT` | Tether |
202
+ | `KES` | Kenyan Shilling |
203
+ | `GHS` | Ghanaian Cedi |
204
+ | `ZAR` | South African Rand |
205
+
206
+ ## License
207
+
208
+ MIT
@@ -0,0 +1,339 @@
1
+ interface PuulPartnerConfig {
2
+ /** Your partner client ID */
3
+ clientId: string;
4
+ /** Your partner client secret */
5
+ clientSecret: string;
6
+ /** Base URL for the Puul API. Defaults to https://api.joinpuul.com/api/v1 */
7
+ baseUrl?: string;
8
+ /** Request timeout in ms. Defaults to 30000 */
9
+ timeoutMs?: number;
10
+ }
11
+ interface AccessTokenResponse {
12
+ access_token: string;
13
+ token_type: string;
14
+ expires_in: number;
15
+ }
16
+ interface CreateLinkTokenParams {
17
+ externalUserId: string;
18
+ email?: string;
19
+ phone?: string;
20
+ countryCode?: string;
21
+ }
22
+ interface LinkTokenResponse {
23
+ linkToken: string;
24
+ expiresAt: string;
25
+ }
26
+ interface SessionResponse {
27
+ access_token: string;
28
+ token_type: string;
29
+ expires_in: number;
30
+ user_id: string;
31
+ }
32
+ interface MarketOutcome {
33
+ id: string;
34
+ slug: string;
35
+ label: string;
36
+ pool: {
37
+ pool_amount: string;
38
+ total_bets: number;
39
+ };
40
+ }
41
+ interface Market {
42
+ id: string;
43
+ question: string;
44
+ description: string;
45
+ category: string;
46
+ status: string;
47
+ currency: string;
48
+ close_time: string;
49
+ freeze_at: string | null;
50
+ target_countries: string[];
51
+ outcomes: MarketOutcome[];
52
+ }
53
+ type PredictionStatus = 'OPEN' | 'WON' | 'LOST' | 'VOIDED';
54
+ type Currency = 'NGN' | 'USDC' | 'USDT' | 'KES' | 'GHS' | 'ZAR';
55
+ interface PlacePredictionParams {
56
+ marketId: string;
57
+ outcomeId: string;
58
+ stakeAmount: number;
59
+ stakeCurrency: Currency;
60
+ idempotencyKey: string;
61
+ /** Optional quote ID for slippage protection */
62
+ quoteId?: string;
63
+ /** Optional FX quote ID */
64
+ fxQuoteId?: string;
65
+ /** Max slippage in basis points (100 = 1%) */
66
+ maxSlippageBps?: number;
67
+ }
68
+ interface CreateQuoteParams {
69
+ marketId: string;
70
+ outcomeId: string;
71
+ stakeAmount: number;
72
+ stakeCurrency: Currency;
73
+ }
74
+ interface QuoteResponse {
75
+ quoteId: string;
76
+ estimatedReturn: number;
77
+ multiplier: string;
78
+ expiresAt: string;
79
+ }
80
+ interface Prediction {
81
+ id: string;
82
+ market_id: string;
83
+ market_question: string | null;
84
+ market_close_time: string | null;
85
+ market_freeze_at: string | null;
86
+ outcome_id: string;
87
+ outcome_slug: string | null;
88
+ outcome_label: string | null;
89
+ user_external_id: string | null;
90
+ stake_amount: string;
91
+ stake_currency: string;
92
+ status: PredictionStatus;
93
+ estimated_return: string;
94
+ live_estimated_return: string | number;
95
+ estimated_multiplier: string;
96
+ final_return: string | null;
97
+ final_multiplier: string | null;
98
+ placed_at: string;
99
+ idempotency_key: string;
100
+ }
101
+ interface PredictionListResponse {
102
+ predictions: Prediction[];
103
+ total: number;
104
+ limit: number;
105
+ }
106
+ interface CreatePendingPredictionParams {
107
+ marketId: string;
108
+ outcomeId?: string;
109
+ /** @deprecated Use outcomeId for multi-outcome markets */
110
+ side?: 'YES' | 'NO';
111
+ stakeAmount: number;
112
+ stakeCurrency: Currency;
113
+ userExternalId: string;
114
+ idempotencyKey: string;
115
+ maxSlippageBps?: number;
116
+ /** TTL in seconds. Default: 300 */
117
+ ttlSeconds?: number;
118
+ }
119
+ interface PendingPrediction {
120
+ id: string;
121
+ status: string;
122
+ userExternalId: string;
123
+ marketId: string;
124
+ outcomeId: string | null;
125
+ side: string | null;
126
+ stakeAmount: number;
127
+ stakeCurrency: string;
128
+ paymentReference: string;
129
+ expiresAt: string;
130
+ predictionId: string | null;
131
+ createdAt: string;
132
+ fundedAt: string | null;
133
+ placedAt: string | null;
134
+ errorMessage: string | null;
135
+ }
136
+ interface WalletBalance {
137
+ balance: number;
138
+ currency: string;
139
+ }
140
+ interface DepositAccountInfo {
141
+ accountNumber: string;
142
+ accountName: string;
143
+ bankName: string;
144
+ currency: string;
145
+ }
146
+ interface DepositParams {
147
+ amount: number;
148
+ currency?: string;
149
+ idempotency_key: string;
150
+ reference?: string;
151
+ }
152
+ type WebhookEventType = 'prediction.settled' | 'deposit.confirmed' | 'withdrawal.completed';
153
+ interface WebhookEvent<T = {
154
+ [key: string]: unknown;
155
+ }> {
156
+ event: WebhookEventType;
157
+ timestamp: string;
158
+ data: T;
159
+ }
160
+ interface PredictionSettledData {
161
+ prediction_id: string;
162
+ market_id: string;
163
+ outcome_id: string;
164
+ outcome_slug: string;
165
+ user_external_id: string;
166
+ status: 'WON' | 'LOST' | 'VOIDED';
167
+ stake_amount: string;
168
+ stake_currency: string;
169
+ final_return: string;
170
+ settled_at: string;
171
+ }
172
+ interface PuulApiError {
173
+ statusCode: number;
174
+ message: string | string[];
175
+ code: string;
176
+ requestId?: string;
177
+ details?: unknown;
178
+ retryable?: boolean;
179
+ }
180
+ declare class PuulError extends Error {
181
+ readonly statusCode: number;
182
+ readonly code: string;
183
+ readonly requestId?: string;
184
+ readonly details?: unknown;
185
+ readonly retryable: boolean;
186
+ constructor(apiError: PuulApiError);
187
+ }
188
+
189
+ /**
190
+ * Official Puul Partner SDK client.
191
+ *
192
+ * Zero runtime dependencies — uses native `fetch` (Node 18+).
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * import { PuulPartner } from '@puul/partner-sdk';
197
+ *
198
+ * const puul = new PuulPartner({
199
+ * clientId: 'pk_live_abc123',
200
+ * clientSecret: 'sk_live_secret789',
201
+ * });
202
+ *
203
+ * // List markets
204
+ * const markets = await puul.markets.list();
205
+ *
206
+ * // Place a prediction
207
+ * const prediction = await puul.predictions.place({
208
+ * marketId: markets[0].id,
209
+ * outcomeId: markets[0].outcomes[0].id,
210
+ * stakeAmount: 100000,
211
+ * stakeCurrency: 'NGN',
212
+ * idempotencyKey: 'unique-key-123',
213
+ * });
214
+ * ```
215
+ */
216
+ declare class PuulPartner {
217
+ private readonly config;
218
+ private tokenCache;
219
+ readonly sessions: SessionsAPI;
220
+ readonly markets: MarketsAPI;
221
+ readonly predictions: PredictionsAPI;
222
+ readonly wallet: WalletAPI;
223
+ constructor(config: PuulPartnerConfig);
224
+ /** Get a valid access token, refreshing if needed */
225
+ getAccessToken(): Promise<string>;
226
+ /** Authenticated request */
227
+ request<T>(method: string, path: string, body?: unknown): Promise<T>;
228
+ /** Raw HTTP request */
229
+ private rawRequest;
230
+ }
231
+ declare class SessionsAPI {
232
+ private readonly client;
233
+ constructor(client: PuulPartner);
234
+ /** Create a link token for user linking */
235
+ createLinkToken(params: CreateLinkTokenParams): Promise<LinkTokenResponse>;
236
+ /** Exchange a link token for a user session */
237
+ create(linkToken: string): Promise<SessionResponse>;
238
+ }
239
+ declare class MarketsAPI {
240
+ private readonly client;
241
+ constructor(client: PuulPartner);
242
+ /** List all live markets, optionally filtered by country */
243
+ list(countries?: string[]): Promise<Market[]>;
244
+ }
245
+ declare class PredictionsAPI {
246
+ private readonly client;
247
+ constructor(client: PuulPartner);
248
+ /** Create a binding quote for slippage protection (expires in 10s) */
249
+ createQuote(params: CreateQuoteParams): Promise<QuoteResponse>;
250
+ /** Place a prediction on a market outcome */
251
+ place(params: PlacePredictionParams): Promise<Prediction>;
252
+ /** Get a specific prediction by ID */
253
+ get(predictionId: string): Promise<Prediction>;
254
+ /** List user predictions with optional filters */
255
+ list(options?: {
256
+ status?: PredictionStatus;
257
+ limit?: number;
258
+ }): Promise<PredictionListResponse>;
259
+ /** Create a pending prediction (JIT funding flow) */
260
+ createPending(params: CreatePendingPredictionParams): Promise<PendingPrediction>;
261
+ /** Get pending prediction status */
262
+ getPending(pendingId: string): Promise<PendingPrediction>;
263
+ }
264
+ declare class WalletAPI {
265
+ private readonly client;
266
+ constructor(client: PuulPartner);
267
+ /** Get partner deposit account details */
268
+ getDepositAccount(): Promise<DepositAccountInfo>;
269
+ /** Get user wallet balance */
270
+ getBalance(): Promise<WalletBalance>;
271
+ /** Get omnibus wallet balance */
272
+ getOmnibusBalance(currency?: string): Promise<WalletBalance>;
273
+ /** Deposit funds to a linked user wallet */
274
+ deposit(params: DepositParams): Promise<unknown>;
275
+ /** Get withdrawal fee estimate */
276
+ getWithdrawalFees(currency: string, amount: number): Promise<unknown>;
277
+ }
278
+
279
+ /**
280
+ * Verify a webhook signature from Puul.
281
+ *
282
+ * @param payload - The raw request body as a string
283
+ * @param signature - The value of the X-Puul-Signature header
284
+ * @param secret - Your webhook secret
285
+ * @returns true if the signature is valid
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * import { verifyWebhookSignature } from '@puul/partner-sdk';
290
+ *
291
+ * app.post('/webhooks/puul', (req, res) => {
292
+ * const isValid = verifyWebhookSignature(
293
+ * req.body, // raw string body
294
+ * req.headers['x-puul-signature'],
295
+ * process.env.PUUL_WEBHOOK_SECRET,
296
+ * );
297
+ *
298
+ * if (!isValid) {
299
+ * return res.status(401).send('Invalid signature');
300
+ * }
301
+ *
302
+ * // Process the event...
303
+ * res.status(200).send('OK');
304
+ * });
305
+ * ```
306
+ */
307
+ declare function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;
308
+ /**
309
+ * Parse and type a webhook event body.
310
+ *
311
+ * @param body - The parsed JSON body of the webhook request
312
+ * @returns A typed WebhookEvent object
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * import { parseWebhookEvent } from '@puul/partner-sdk';
317
+ *
318
+ * const event = parseWebhookEvent(req.body);
319
+ *
320
+ * if (event.event === 'prediction.settled') {
321
+ * const { prediction_id, status, final_return } = event.data;
322
+ * // Handle settlement...
323
+ * }
324
+ * ```
325
+ */
326
+ declare function parseWebhookEvent(body: unknown): WebhookEvent;
327
+ /**
328
+ * Type guard for prediction.settled events.
329
+ * Returns true if the event type is 'prediction.settled'.
330
+ * Use this to narrow the type before accessing `.data` as PredictionSettledData.
331
+ */
332
+ declare function isPredictionSettledEvent(event: WebhookEvent): boolean;
333
+ /**
334
+ * Convenience cast for prediction.settled event data.
335
+ * Call after verifying with `isPredictionSettledEvent`.
336
+ */
337
+ declare function asPredictionSettledData(event: WebhookEvent): PredictionSettledData;
338
+
339
+ export { type AccessTokenResponse, type CreateLinkTokenParams, type CreatePendingPredictionParams, type CreateQuoteParams, type Currency, type DepositAccountInfo, type DepositParams, type LinkTokenResponse, type Market, type MarketOutcome, type PendingPrediction, type PlacePredictionParams, type Prediction, type PredictionListResponse, type PredictionSettledData, type PredictionStatus, type PuulApiError, PuulError, PuulPartner, type PuulPartnerConfig, type QuoteResponse, type SessionResponse, type WalletBalance, type WebhookEvent, type WebhookEventType, asPredictionSettledData, isPredictionSettledEvent, parseWebhookEvent, verifyWebhookSignature };