@alpha-arcade/sdk 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -337,6 +337,150 @@ for (const m of rewardMarkets) {
337
337
 
338
338
  ---
339
339
 
340
+ ### WebSocket Streams
341
+
342
+ Real-time data streams via WebSocket. No API key or auth required. Replaces polling with push-based updates.
343
+
344
+ The SDK connects to the public platform websocket at `wss://wss.platform.alphaarcade.com`. The first
345
+ subscription is sent in the connection query string, and any later subscribe or unsubscribe calls use the
346
+ server's control-message envelope:
347
+
348
+ ```json
349
+ {
350
+ "id": "request-id",
351
+ "method": "SUBSCRIBE",
352
+ "params": [
353
+ { "stream": "get-orderbook", "slug": "will-btc-hit-100k" }
354
+ ]
355
+ }
356
+ ```
357
+
358
+ Supported public streams:
359
+
360
+ - `get-live-markets`
361
+ - `get-market` with `slug`
362
+ - `get-orderbook` with `slug`
363
+ - `get-wallet-orders` with `wallet`
364
+
365
+ ```typescript
366
+ import { AlphaWebSocket } from '@alpha-arcade/sdk';
367
+
368
+ // Node.js 22+ and browsers — native WebSocket, nothing extra needed
369
+ const ws = new AlphaWebSocket();
370
+
371
+ // Node.js < 22 — install `ws` and pass it in:
372
+ // npm install ws
373
+ import WebSocket from 'ws';
374
+ const ws = new AlphaWebSocket({ WebSocket });
375
+
376
+ // Subscribe to orderbook updates (~5s snapshots)
377
+ const unsub = ws.subscribeOrderbook('will-btc-hit-100k', (event) => {
378
+ console.log('Orderbook:', event.orderbook);
379
+ });
380
+
381
+ // Unsubscribe when done
382
+ unsub();
383
+
384
+ // Close the connection
385
+ ws.close();
386
+ ```
387
+
388
+ #### `subscribeLiveMarkets(callback)`
389
+
390
+ Receive incremental diffs whenever market probabilities change.
391
+
392
+ ```typescript
393
+ ws.subscribeLiveMarkets((event) => {
394
+ console.log('Markets changed at', event.ts, event);
395
+ });
396
+ ```
397
+
398
+ #### `subscribeMarket(slug, callback)`
399
+
400
+ Receive change events for a single market. Uses the market **slug** (not `marketAppId`) — see note on `subscribeOrderbook` below.
401
+
402
+ ```typescript
403
+ ws.subscribeMarket('will-btc-hit-100k', (event) => {
404
+ console.log('Market update:', event);
405
+ });
406
+ ```
407
+
408
+ #### `subscribeOrderbook(slug, callback)`
409
+
410
+ Receive full orderbook snapshots on every change (~5s interval). Replaces on-chain polling.
411
+
412
+ **Note:** The WebSocket API uses market **slugs** (URL-friendly names like `"will-btc-hit-100k"`), not `marketAppId` numbers. You can get a market's slug from the `slug` field on `Market` objects returned by `getLiveMarkets()` or `getMarket()`.
413
+
414
+ ```typescript
415
+ ws.subscribeOrderbook('will-btc-hit-100k', (event) => {
416
+ // Top-level bids/asks use decimal prices (cents)
417
+ // Nested yes/no use raw microunit prices with escrowAppId and owner
418
+ for (const [appId, book] of Object.entries(event.orderbook)) {
419
+ console.log(`App ${appId}: spread=${book.spread}`);
420
+ console.log(' Bids:', book.bids);
421
+ console.log(' Yes bids:', book.yes.bids);
422
+ }
423
+ });
424
+ ```
425
+
426
+ #### `subscribeWalletOrders(wallet, callback)`
427
+
428
+ Receive updates when orders for a wallet are created or modified.
429
+
430
+ ```typescript
431
+ ws.subscribeWalletOrders('MMU6X...', (event) => {
432
+ console.log('Wallet orders changed:', event);
433
+ });
434
+ ```
435
+
436
+ #### Unsubscribing
437
+
438
+ Each `subscribe*` method returns an unsubscribe function. Call it to stop receiving events for that stream:
439
+
440
+ ```typescript
441
+ const unsub = ws.subscribeOrderbook('my-market', (event) => { /* ... */ });
442
+
443
+ // Later, stop listening
444
+ unsub();
445
+ ```
446
+
447
+ #### Control Methods
448
+
449
+ ```typescript
450
+ // List active subscriptions on this connection
451
+ const subs = await ws.listSubscriptions();
452
+
453
+ // Query server properties (`heartbeat` or `limits`)
454
+ const props = await ws.getProperty('heartbeat');
455
+ ```
456
+
457
+ #### Configuration
458
+
459
+ ```typescript
460
+ import WebSocket from 'ws'; // Only needed on Node.js < 22
461
+
462
+ const ws = new AlphaWebSocket({
463
+ WebSocket, // Pass `ws` on Node.js < 22 (not needed in browsers or Node 22+)
464
+ url: 'wss://custom-endpoint.example.com', // Override default URL
465
+ reconnect: true, // Auto-reconnect (default: true)
466
+ maxReconnectAttempts: 10, // Give up after 10 retries (default: Infinity)
467
+ heartbeatIntervalMs: 60_000, // Ping interval in ms (default: 60000)
468
+ });
469
+ ```
470
+
471
+ #### Connection Details
472
+
473
+ | Setting | Value |
474
+ |---------|-------|
475
+ | Heartbeat | 60s (auto-handled) |
476
+ | Idle timeout | 180s |
477
+ | Rate limit | 5 messages/sec/connection |
478
+ | Reconnect | Exponential backoff (1s → 30s max) |
479
+
480
+ The client automatically responds to server pings, sends keepalive pings, and reconnects with exponential backoff on unexpected disconnects. All active subscriptions are restored after reconnect.
481
+
482
+ ---
483
+
340
484
  ### Utility Functions
341
485
 
342
486
  These are exported for advanced users:
package/dist/index.cjs CHANGED
@@ -2125,6 +2125,7 @@ var calculateMatchingOrders = (orderbook, isBuying, isYes, quantity, price, slip
2125
2125
 
2126
2126
  // src/constants.ts
2127
2127
  var DEFAULT_API_BASE_URL = "https://platform.alphaarcade.com/api";
2128
+ var DEFAULT_WSS_BASE_URL = "wss://wss.platform.alphaarcade.com";
2128
2129
  var DEFAULT_MARKET_CREATOR_ADDRESS = "5P5Y6HTWUNG2E3VXBQDZN3ENZD3JPAIR5PKT3LOYJAPAUKOLFD6KANYTRY";
2129
2130
 
2130
2131
  // src/modules/orderbook.ts
@@ -3366,9 +3367,275 @@ var AlphaClient = class {
3366
3367
  }
3367
3368
  };
3368
3369
 
3370
+ // src/websocket.ts
3371
+ var WS_OPEN = 1;
3372
+ var resolveWebSocket = (provided) => {
3373
+ if (provided) return provided;
3374
+ if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
3375
+ return globalThis.WebSocket;
3376
+ }
3377
+ throw new Error(
3378
+ 'No WebSocket implementation found. On Node.js < 22, install the "ws" package and pass it: new AlphaWebSocket({ WebSocket: require("ws") })'
3379
+ );
3380
+ };
3381
+ var AlphaWebSocket = class {
3382
+ url;
3383
+ reconnectEnabled;
3384
+ maxReconnectAttempts;
3385
+ heartbeatIntervalMs;
3386
+ WebSocketImpl;
3387
+ ws = null;
3388
+ subscriptions = /* @__PURE__ */ new Map();
3389
+ pendingRequests = /* @__PURE__ */ new Map();
3390
+ heartbeatTimer = null;
3391
+ reconnectTimer = null;
3392
+ reconnectAttempts = 0;
3393
+ intentionallyClosed = false;
3394
+ connectPromise = null;
3395
+ constructor(config) {
3396
+ this.url = config?.url ?? DEFAULT_WSS_BASE_URL;
3397
+ this.reconnectEnabled = config?.reconnect ?? true;
3398
+ this.maxReconnectAttempts = config?.maxReconnectAttempts ?? Infinity;
3399
+ this.heartbeatIntervalMs = config?.heartbeatIntervalMs ?? 6e4;
3400
+ this.WebSocketImpl = resolveWebSocket(config?.WebSocket);
3401
+ }
3402
+ /** Whether the WebSocket is currently open and connected */
3403
+ get connected() {
3404
+ return this.ws?.readyState === WS_OPEN;
3405
+ }
3406
+ // ============================================
3407
+ // Subscribe Methods
3408
+ // ============================================
3409
+ /**
3410
+ * Subscribe to live market probability updates (incremental diffs).
3411
+ * @returns An unsubscribe function
3412
+ */
3413
+ subscribeLiveMarkets(callback) {
3414
+ return this.subscribe("get-live-markets", {}, "markets_changed", callback);
3415
+ }
3416
+ /**
3417
+ * Subscribe to change events for a single market.
3418
+ * @param slug - The market slug
3419
+ * @returns An unsubscribe function
3420
+ */
3421
+ subscribeMarket(slug, callback) {
3422
+ return this.subscribe("get-market", { slug }, "market_changed", callback);
3423
+ }
3424
+ /**
3425
+ * Subscribe to full orderbook snapshots (~5s interval on changes).
3426
+ * @param slug - The market slug
3427
+ * @returns An unsubscribe function
3428
+ */
3429
+ subscribeOrderbook(slug, callback) {
3430
+ return this.subscribe("get-orderbook", { slug }, "orderbook_changed", callback);
3431
+ }
3432
+ /**
3433
+ * Subscribe to wallet order updates.
3434
+ * @param wallet - The wallet address
3435
+ * @returns An unsubscribe function
3436
+ */
3437
+ subscribeWalletOrders(wallet, callback) {
3438
+ return this.subscribe("get-wallet-orders", { wallet }, "wallet_orders_changed", callback);
3439
+ }
3440
+ // ============================================
3441
+ // Control Methods
3442
+ // ============================================
3443
+ /** Query the server for the list of active subscriptions on this connection */
3444
+ listSubscriptions() {
3445
+ return this.sendRequest({ method: "LIST_SUBSCRIPTIONS" });
3446
+ }
3447
+ /** Query a server property (e.g. "heartbeat", "limits") */
3448
+ getProperty(property) {
3449
+ return this.sendRequest({ method: "GET_PROPERTY", params: [property] });
3450
+ }
3451
+ // ============================================
3452
+ // Lifecycle
3453
+ // ============================================
3454
+ /** Open the WebSocket connection. Called automatically on first subscribe. */
3455
+ connect() {
3456
+ if (this.connectPromise) return this.connectPromise;
3457
+ this.connectPromise = this.doConnect();
3458
+ return this.connectPromise;
3459
+ }
3460
+ /** Close the connection and clean up all resources */
3461
+ close() {
3462
+ this.intentionallyClosed = true;
3463
+ this.clearTimers();
3464
+ this.subscriptions.clear();
3465
+ for (const [, req] of this.pendingRequests) {
3466
+ clearTimeout(req.timer);
3467
+ req.reject(new Error("WebSocket closed"));
3468
+ }
3469
+ this.pendingRequests.clear();
3470
+ if (this.ws) {
3471
+ this.ws.close();
3472
+ this.ws = null;
3473
+ }
3474
+ this.connectPromise = null;
3475
+ }
3476
+ // ============================================
3477
+ // Internal
3478
+ // ============================================
3479
+ buildStreamKey(stream, params) {
3480
+ const parts = [stream, ...Object.entries(params).sort().map(([k, v]) => `${k}=${v}`)];
3481
+ return parts.join("&");
3482
+ }
3483
+ buildQueryString() {
3484
+ const subs = [...this.subscriptions.values()];
3485
+ if (subs.length === 0) return "";
3486
+ const first = subs[0];
3487
+ const params = new URLSearchParams({ stream: first.stream, ...first.params });
3488
+ return "?" + params.toString();
3489
+ }
3490
+ subscribe(stream, params, eventType, callback) {
3491
+ const key = this.buildStreamKey(stream, params);
3492
+ this.subscriptions.set(key, { stream, params, callback, eventType });
3493
+ if (this.connected) {
3494
+ this.sendSubscribe(stream, params);
3495
+ } else {
3496
+ this.connect();
3497
+ }
3498
+ return () => {
3499
+ this.subscriptions.delete(key);
3500
+ if (this.connected) {
3501
+ this.sendUnsubscribe(stream, params);
3502
+ }
3503
+ };
3504
+ }
3505
+ async doConnect() {
3506
+ this.intentionallyClosed = false;
3507
+ return new Promise((resolve, reject) => {
3508
+ const qs = this.buildQueryString();
3509
+ const ws = new this.WebSocketImpl(this.url + qs);
3510
+ ws.onopen = () => {
3511
+ this.ws = ws;
3512
+ this.reconnectAttempts = 0;
3513
+ this.startHeartbeat();
3514
+ const subs = [...this.subscriptions.values()];
3515
+ for (const sub of subs.slice(1)) {
3516
+ this.sendSubscribe(sub.stream, sub.params);
3517
+ }
3518
+ resolve();
3519
+ };
3520
+ ws.onmessage = (event) => {
3521
+ this.handleMessage(event.data);
3522
+ };
3523
+ ws.onclose = () => {
3524
+ this.ws = null;
3525
+ this.connectPromise = null;
3526
+ this.stopHeartbeat();
3527
+ if (!this.intentionallyClosed) {
3528
+ this.scheduleReconnect();
3529
+ }
3530
+ };
3531
+ ws.onerror = (err) => {
3532
+ if (!this.ws) {
3533
+ reject(new Error("WebSocket connection failed"));
3534
+ }
3535
+ };
3536
+ });
3537
+ }
3538
+ handleMessage(raw) {
3539
+ let msg;
3540
+ try {
3541
+ msg = JSON.parse(raw);
3542
+ } catch {
3543
+ return;
3544
+ }
3545
+ if (msg.type === "ping") {
3546
+ this.send({ method: "PONG" });
3547
+ return;
3548
+ }
3549
+ const responseId = typeof msg.id === "string" ? msg.id : typeof msg.requestId === "string" ? msg.requestId : null;
3550
+ if (responseId && this.pendingRequests.has(responseId)) {
3551
+ const req = this.pendingRequests.get(responseId);
3552
+ this.pendingRequests.delete(responseId);
3553
+ clearTimeout(req.timer);
3554
+ req.resolve(msg);
3555
+ return;
3556
+ }
3557
+ const eventType = msg.type;
3558
+ if (!eventType) return;
3559
+ for (const sub of this.subscriptions.values()) {
3560
+ if (sub.eventType === eventType) {
3561
+ try {
3562
+ sub.callback(msg);
3563
+ } catch {
3564
+ }
3565
+ }
3566
+ }
3567
+ }
3568
+ sendSubscribe(stream, params) {
3569
+ this.send({ method: "SUBSCRIBE", params: [{ stream, ...params }] });
3570
+ }
3571
+ sendUnsubscribe(stream, params) {
3572
+ this.send({ method: "UNSUBSCRIBE", params: [{ stream, ...params }] });
3573
+ }
3574
+ sendRequest(payload, timeoutMs = 1e4) {
3575
+ const requestId = crypto.randomUUID();
3576
+ return new Promise((resolve, reject) => {
3577
+ const timer = setTimeout(() => {
3578
+ this.pendingRequests.delete(requestId);
3579
+ reject(new Error("Request timed out"));
3580
+ }, timeoutMs);
3581
+ this.pendingRequests.set(requestId, { resolve, reject, timer });
3582
+ if (!this.connected) {
3583
+ this.connect().then(() => {
3584
+ this.send({ ...payload, id: requestId });
3585
+ }).catch(reject);
3586
+ } else {
3587
+ this.send({ ...payload, id: requestId });
3588
+ }
3589
+ });
3590
+ }
3591
+ send(data) {
3592
+ if (this.ws?.readyState === WS_OPEN) {
3593
+ this.ws.send(JSON.stringify(data));
3594
+ }
3595
+ }
3596
+ // ============================================
3597
+ // Heartbeat
3598
+ // ============================================
3599
+ startHeartbeat() {
3600
+ this.stopHeartbeat();
3601
+ this.heartbeatTimer = setInterval(() => {
3602
+ this.send({ method: "PING" });
3603
+ }, this.heartbeatIntervalMs);
3604
+ }
3605
+ stopHeartbeat() {
3606
+ if (this.heartbeatTimer) {
3607
+ clearInterval(this.heartbeatTimer);
3608
+ this.heartbeatTimer = null;
3609
+ }
3610
+ }
3611
+ // ============================================
3612
+ // Reconnect
3613
+ // ============================================
3614
+ scheduleReconnect() {
3615
+ if (!this.reconnectEnabled) return;
3616
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
3617
+ const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
3618
+ this.reconnectAttempts++;
3619
+ this.reconnectTimer = setTimeout(() => {
3620
+ this.reconnectTimer = null;
3621
+ this.connect().catch(() => {
3622
+ });
3623
+ }, delay);
3624
+ }
3625
+ clearTimers() {
3626
+ this.stopHeartbeat();
3627
+ if (this.reconnectTimer) {
3628
+ clearTimeout(this.reconnectTimer);
3629
+ this.reconnectTimer = null;
3630
+ }
3631
+ }
3632
+ };
3633
+
3369
3634
  exports.AlphaClient = AlphaClient;
3635
+ exports.AlphaWebSocket = AlphaWebSocket;
3370
3636
  exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
3371
3637
  exports.DEFAULT_MARKET_CREATOR_ADDRESS = DEFAULT_MARKET_CREATOR_ADDRESS;
3638
+ exports.DEFAULT_WSS_BASE_URL = DEFAULT_WSS_BASE_URL;
3372
3639
  exports.calculateFee = calculateFee;
3373
3640
  exports.calculateFeeFromTotal = calculateFeeFromTotal;
3374
3641
  exports.calculateMatchingOrders = calculateMatchingOrders;