@taphubhq/sdk-core 0.14.0 → 0.15.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/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AgencyPairModule: () => AgencyPairModule,
33
34
  AuthModule: () => AuthModule,
34
35
  BidModule: () => BidModule,
35
36
  GameModule: () => GameModule,
@@ -130,6 +131,61 @@ var TaphubEventBus = class {
130
131
  }
131
132
  };
132
133
 
134
+ // src/modules/agencyPair/normalise.ts
135
+ function normaliseAgencyPairs(rows) {
136
+ if (!Array.isArray(rows)) {
137
+ throw new TaphubServerError("Invalid response from server", {
138
+ code: "InvalidResponse"
139
+ });
140
+ }
141
+ return rows.map((row) => ({
142
+ gamePairId: row.gamePairId,
143
+ pair: row.pair,
144
+ thumb: row.thumb,
145
+ ordering: Number(row.ordering),
146
+ maxCoef: Number(row.maxCoef),
147
+ currentPrice: Number(row.currentPrice),
148
+ currentVol24h: Number(row.currentVol24h)
149
+ }));
150
+ }
151
+
152
+ // src/modules/agencyPair/queries.ts
153
+ var FETCH_AGENCY_PAIRS_QUERY = `query FetchAgencyPairs($filter: AgencyPairFilter) {
154
+ fetchAgencyPairs(filter: $filter) {
155
+ gamePairId pair thumb ordering maxCoef currentPrice currentVol24h
156
+ }
157
+ }`;
158
+
159
+ // src/modules/agencyPair/index.ts
160
+ var AgencyPairModule = class {
161
+ #graphql;
162
+ constructor(deps) {
163
+ this.#graphql = deps.graphql;
164
+ }
165
+ /**
166
+ * List the agency-in-context's pairs with live derived stats.
167
+ *
168
+ * The agency is resolved server-side from the `x-builder-code` header the SDK
169
+ * sends on every request (no JWT required — Tier 0 anon-readable). There is no
170
+ * argument to target another agency by design.
171
+ */
172
+ async list(args) {
173
+ const variables = {};
174
+ if (args?.filter) {
175
+ const filter = {};
176
+ if (args.filter.pair !== void 0) filter.pair = args.filter.pair;
177
+ if (args.filter.status !== void 0) filter.status = args.filter.status;
178
+ variables.filter = filter;
179
+ }
180
+ const body = await this.#graphql.request(
181
+ FETCH_AGENCY_PAIRS_QUERY,
182
+ variables,
183
+ args?.signal ? { signal: args.signal } : void 0
184
+ );
185
+ return normaliseAgencyPairs(body.fetchAgencyPairs);
186
+ }
187
+ };
188
+
133
189
  // src/modules/auth/normalise.ts
134
190
  function normaliseGoogleResponse(body) {
135
191
  return {
@@ -829,6 +885,9 @@ var GAME_SCOPED_SUFFIXES = ["config", "ideal_config"];
829
885
  var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
830
886
  var TOPIC_PREFIX = "game";
831
887
  var MARKET_TOPIC_PREFIX = "market";
888
+ function agencyPairStatsTopic(aid, gamePairId) {
889
+ return `public/agency/${aid}/pair/${gamePairId}/stats`;
890
+ }
832
891
  function topicFor(gameId, suffix, userId) {
833
892
  if (USER_SCOPED_SUFFIXES.includes(suffix)) {
834
893
  return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
@@ -845,6 +904,7 @@ function createMqttTransport(endpoint, opts = {}) {
845
904
  let client = null;
846
905
  const subscriptions = /* @__PURE__ */ new Map();
847
906
  const candleSubscriptions = /* @__PURE__ */ new Map();
907
+ const statsSubscriptions = /* @__PURE__ */ new Map();
848
908
  const { onLifecycle } = opts;
849
909
  let connectStartedAt = 0;
850
910
  function fireLifecycle(event) {
@@ -902,6 +962,17 @@ function createMqttTransport(endpoint, opts = {}) {
902
962
  candleSub.onCandle(receivedTopic, payload2);
903
963
  return;
904
964
  }
965
+ const statsSub = statsSubscriptions.get(receivedTopic);
966
+ if (statsSub) {
967
+ let payload2;
968
+ try {
969
+ payload2 = JSON.parse(message.toString());
970
+ } catch {
971
+ return;
972
+ }
973
+ statsSub.onStats(receivedTopic, payload2);
974
+ return;
975
+ }
905
976
  let matched;
906
977
  for (const sub of subscriptions.values()) {
907
978
  if (sub.topics.includes(receivedTopic)) {
@@ -962,6 +1033,20 @@ function createMqttTransport(endpoint, opts = {}) {
962
1033
  if (client) client.unsubscribe(entry.topic);
963
1034
  candleSubscriptions.delete(pair);
964
1035
  },
1036
+ subscribeAgencyPairStats(aid, gamePairId, onStats) {
1037
+ const topic = agencyPairStatsTopic(aid, gamePairId);
1038
+ if (statsSubscriptions.has(topic)) return;
1039
+ const mqttClient = ensureConnected();
1040
+ statsSubscriptions.set(topic, { topic, onStats });
1041
+ mqttClient.subscribe(topic, { qos: 0 });
1042
+ },
1043
+ unsubscribeAgencyPairStats(aid, gamePairId) {
1044
+ const topic = agencyPairStatsTopic(aid, gamePairId);
1045
+ const entry = statsSubscriptions.get(topic);
1046
+ if (!entry) return;
1047
+ if (client) client.unsubscribe(entry.topic);
1048
+ statsSubscriptions.delete(topic);
1049
+ },
965
1050
  unsubscribeAll(gameId, userId) {
966
1051
  const matches = entriesForGame(gameId);
967
1052
  if (matches.length === 0) return;
@@ -996,9 +1081,13 @@ function createMqttTransport(endpoint, opts = {}) {
996
1081
  for (const entry of candleSubscriptions.values()) {
997
1082
  client.unsubscribe(entry.topic);
998
1083
  }
1084
+ for (const entry of statsSubscriptions.values()) {
1085
+ client.unsubscribe(entry.topic);
1086
+ }
999
1087
  }
1000
1088
  subscriptions.clear();
1001
1089
  candleSubscriptions.clear();
1090
+ statsSubscriptions.clear();
1002
1091
  if (client) {
1003
1092
  client.end(true);
1004
1093
  client = null;
@@ -1119,11 +1208,13 @@ function normaliseUserId2(userId) {
1119
1208
  var RealtimeModule = class extends import_eventemitter32.default {
1120
1209
  #transport;
1121
1210
  #entries = /* @__PURE__ */ new Map();
1211
+ #agencyId;
1122
1212
  constructor(mqttEndpointOrOptions) {
1123
1213
  super();
1124
1214
  if (typeof mqttEndpointOrOptions === "string") {
1125
1215
  this.#transport = createMqttTransport(mqttEndpointOrOptions);
1126
1216
  } else {
1217
+ this.#agencyId = mqttEndpointOrOptions.agencyId;
1127
1218
  this.#transport = mqttEndpointOrOptions.transport ?? createMqttTransport(mqttEndpointOrOptions.mqttEndpoint, {
1128
1219
  onLifecycle: mqttEndpointOrOptions.onMqttLifecycle
1129
1220
  });
@@ -1207,6 +1298,33 @@ var RealtimeModule = class extends import_eventemitter32.default {
1207
1298
  unsubscribeCandle(pair) {
1208
1299
  this.#transport.unsubscribeCandle(pair);
1209
1300
  }
1301
+ /**
1302
+ * Subscribe to the live per-pair stats stream for the client's own agency.
1303
+ * Scoped to the agency the client was constructed with — there is no argument
1304
+ * to target another agency by design.
1305
+ */
1306
+ subscribeAgencyPairStats(gamePairId, handler) {
1307
+ if (!this.#agencyId) {
1308
+ throw new TaphubError("agencyId is required to subscribe to agency pair stats", {
1309
+ code: "AgencyIdRequired"
1310
+ });
1311
+ }
1312
+ const onStats = (_topic, raw) => {
1313
+ const p = raw;
1314
+ handler({
1315
+ price: Number(p.price),
1316
+ vol24h: Number(p.vol24h),
1317
+ maxCoef: Number(p.maxCoef),
1318
+ ts: Number(p.ts)
1319
+ });
1320
+ };
1321
+ this.#transport.subscribeAgencyPairStats(this.#agencyId, gamePairId, onStats);
1322
+ }
1323
+ /** Unsubscribe from the per-pair stats stream for the client's own agency. */
1324
+ unsubscribeAgencyPairStats(gamePairId) {
1325
+ if (!this.#agencyId) return;
1326
+ this.#transport.unsubscribeAgencyPairStats(this.#agencyId, gamePairId);
1327
+ }
1210
1328
  disconnect() {
1211
1329
  for (const entry of this.#entries.values()) {
1212
1330
  entry.channel.removeAllListeners();
@@ -2271,6 +2389,8 @@ var TaphubClient = class {
2271
2389
  bid;
2272
2390
  /** @readonly Leaderboard module — reassignment has no effect at runtime. */
2273
2391
  leaderboard;
2392
+ /** @readonly Agency pair module — reassignment has no effect at runtime. */
2393
+ agencyPairs;
2274
2394
  /** @readonly Locale module — reassignment has no effect at runtime. */
2275
2395
  locale;
2276
2396
  /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
@@ -2351,9 +2471,11 @@ var TaphubClient = class {
2351
2471
  this.game = new GameModule({ graphql: this.#graphql });
2352
2472
  this.bid = new BidModule({ graphql: this.#graphql });
2353
2473
  this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
2474
+ this.agencyPairs = new AgencyPairModule({ graphql: this.#graphql });
2354
2475
  this.locale = new LocaleModule({ graphql: this.#graphql });
2355
2476
  this.realtime = config.mqttEndpoint ? new RealtimeModule({
2356
2477
  mqttEndpoint: config.mqttEndpoint,
2478
+ agencyId: this.agencyId,
2357
2479
  onMqttLifecycle: createMqttProbe(this.network)
2358
2480
  }) : void 0;
2359
2481
  attachConnectionProbe(this.network);
@@ -2387,6 +2509,12 @@ var TaphubClient = class {
2387
2509
  enumerable: true,
2388
2510
  configurable: false
2389
2511
  });
2512
+ Object.defineProperty(this, "agencyPairs", {
2513
+ value: this.agencyPairs,
2514
+ writable: false,
2515
+ enumerable: true,
2516
+ configurable: false
2517
+ });
2390
2518
  if (this.realtime) {
2391
2519
  Object.defineProperty(this, "realtime", {
2392
2520
  value: this.realtime,
@@ -2523,6 +2651,7 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
2523
2651
  }
2524
2652
  // Annotate the CommonJS export names for ESM import in node:
2525
2653
  0 && (module.exports = {
2654
+ AgencyPairModule,
2526
2655
  AuthModule,
2527
2656
  BidModule,
2528
2657
  GameModule,
package/dist/index.d.mts CHANGED
@@ -63,6 +63,49 @@ interface GraphQLTransport {
63
63
  request<T>(query: string, variables?: Record<string, unknown>, opts?: GraphQLRequestOpts): Promise<T>;
64
64
  }
65
65
 
66
+ type AgencyPairStatus = 'active' | 'paused' | 'ended';
67
+ interface AgencyPairFilter {
68
+ /** Exact match on the display pair name, e.g. "ETH/USD". */
69
+ pair?: string;
70
+ /** Exact match on the agency pair status. */
71
+ status?: AgencyPairStatus;
72
+ }
73
+ interface AgencyPairStats {
74
+ /** game_pairs.id slug, e.g. "grid-ETH-USD". Use to build the stats MQTT topic. */
75
+ gamePairId: string;
76
+ /** Display name, e.g. "ETH/USD". */
77
+ pair: string;
78
+ /** Icon URL. */
79
+ thumb: string;
80
+ /** Lower = earlier in the displayed list. */
81
+ ordering: number;
82
+ /** config.constraints.maxCoef. */
83
+ maxCoef: number;
84
+ /** Latest candle close price; 0 when no candle exists. */
85
+ currentPrice: number;
86
+ /** Sliding 24h bid-amount sum; 0 when the worker snapshot is missing or stale. */
87
+ currentVol24h: number;
88
+ }
89
+
90
+ interface AgencyPairModuleDeps {
91
+ graphql: GraphQLTransport;
92
+ }
93
+ declare class AgencyPairModule {
94
+ #private;
95
+ constructor(deps: AgencyPairModuleDeps);
96
+ /**
97
+ * List the agency-in-context's pairs with live derived stats.
98
+ *
99
+ * The agency is resolved server-side from the `x-builder-code` header the SDK
100
+ * sends on every request (no JWT required — Tier 0 anon-readable). There is no
101
+ * argument to target another agency by design.
102
+ */
103
+ list(args?: {
104
+ filter?: AgencyPairFilter;
105
+ signal?: AbortSignal;
106
+ }): Promise<AgencyPairStats[]>;
107
+ }
108
+
66
109
  interface RestRequestOpts {
67
110
  signal?: AbortSignal;
68
111
  }
@@ -278,8 +321,8 @@ declare class GameModule {
278
321
  }): Promise<Candle[]>;
279
322
  }
280
323
 
281
- type LeaderboardPeriod = '24h' | '7d' | '30d' | 'all';
282
- type LeaderboardSortBy = 'gain' | 'wins' | 'total_payout' | 'total_wagered' | 'total_bids';
324
+ type LeaderboardPeriod = 'weekly' | 'all_time';
325
+ type LeaderboardSortBy = 'gain' | 'wagered';
283
326
  interface LeaderboardEntry {
284
327
  userId: string;
285
328
  username: string;
@@ -433,7 +476,13 @@ interface MqttWireConfig {
433
476
  max_bid_amount: number;
434
477
  acceptable_bids: number[];
435
478
  }
436
- type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig;
479
+ interface MqttWireAgencyPairStats {
480
+ price: number;
481
+ vol24h: number;
482
+ maxCoef: number;
483
+ ts: number;
484
+ }
485
+ type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig | MqttWireAgencyPairStats;
437
486
  type MqttMessageHandler = (gameId: string, topic: string, payload: MqttWirePayload) => void;
438
487
  type MqttErrorHandler = (err: Error) => void;
439
488
  type MqttLifecycleEvent = {
@@ -453,6 +502,7 @@ type MqttLifecycleEvent = {
453
502
  };
454
503
  type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
455
504
  type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
505
+ type MqttAgencyPairStatsHandler = (topic: string, payload: MqttWirePayload) => void;
456
506
  interface MqttTransport {
457
507
  /**
458
508
  * Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
@@ -477,6 +527,15 @@ interface MqttTransport {
477
527
  subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
478
528
  /** Tear down the candle subscription for a pair. */
479
529
  unsubscribeCandle(pair: string): void;
530
+ /**
531
+ * Subscribe to the public per-pair stats stream for an agency, topic
532
+ * `public/agency/{aid}/pair/{gamePairId}/stats`. `aid` and `gamePairId` are
533
+ * used raw (no slash escaping) to byte-match the backend publisher.
534
+ * Idempotent: multiple calls for the same `(aid, gamePairId)` are deduplicated.
535
+ */
536
+ subscribeAgencyPairStats(aid: string, gamePairId: string, onStats: MqttAgencyPairStatsHandler): void;
537
+ /** Tear down the stats subscription for an `(aid, gamePairId)`. */
538
+ unsubscribeAgencyPairStats(aid: string, gamePairId: string): void;
480
539
  close(): void;
481
540
  }
482
541
 
@@ -549,6 +608,19 @@ interface MqttIdealConfigEvent {
549
608
  currentPrice: number;
550
609
  reason: string;
551
610
  }
611
+ /**
612
+ * Live per-pair stats tick from the public stream
613
+ * `public/agency/{aid}/pair/{gamePairId}/stats`. The backend suppresses
614
+ * unchanged ticks, so a subscriber may not receive an event until price or
615
+ * vol24h actually moves — seed initial values from `agencyPairs.list()`.
616
+ */
617
+ interface MqttAgencyPairStatsEvent {
618
+ price: number;
619
+ vol24h: number;
620
+ maxCoef: number;
621
+ /** Unix epoch **seconds** (matches the backend snapshot `ts`). */
622
+ ts: number;
623
+ }
552
624
 
553
625
  interface GameChannelEvents {
554
626
  candle: [MqttCandleEvent];
@@ -568,6 +640,12 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
568
640
 
569
641
  interface RealtimeModuleOptions {
570
642
  mqttEndpoint: string;
643
+ /**
644
+ * The client's own agency id. Threaded so `subscribeAgencyPairStats` can
645
+ * scope the stats topic to this agency without accepting a target agency from
646
+ * the caller (tenant-scoping mirror of the GQL resolver).
647
+ */
648
+ agencyId?: string;
571
649
  /** @internal For testing only */
572
650
  transport?: MqttTransport;
573
651
  /**
@@ -594,6 +672,14 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
594
672
  subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
595
673
  /** Unsubscribe from candle data for a pair. */
596
674
  unsubscribeCandle(pair: string): void;
675
+ /**
676
+ * Subscribe to the live per-pair stats stream for the client's own agency.
677
+ * Scoped to the agency the client was constructed with — there is no argument
678
+ * to target another agency by design.
679
+ */
680
+ subscribeAgencyPairStats(gamePairId: string, handler: (event: MqttAgencyPairStatsEvent) => void): void;
681
+ /** Unsubscribe from the per-pair stats stream for the client's own agency. */
682
+ unsubscribeAgencyPairStats(gamePairId: string): void;
597
683
  disconnect(): void;
598
684
  }
599
685
 
@@ -702,6 +788,8 @@ declare class TaphubClient {
702
788
  bid: BidModule;
703
789
  /** @readonly Leaderboard module — reassignment has no effect at runtime. */
704
790
  leaderboard: LeaderboardModule;
791
+ /** @readonly Agency pair module — reassignment has no effect at runtime. */
792
+ agencyPairs: AgencyPairModule;
705
793
  /** @readonly Locale module — reassignment has no effect at runtime. */
706
794
  locale: LocaleModule;
707
795
  /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
@@ -791,4 +879,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
791
879
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
792
880
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
793
881
 
794
- export { AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type Game, GameChannel, type GameConfig, GameModule, type GamePairInfo, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type PlaceBidInput, RealtimeModule, type Sample, type SampleReason, type SignalSource, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type Wallet as UserWallet, type Wallet$1 as Wallet, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
882
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type Game, GameChannel, type GameConfig, GameModule, type GamePairInfo, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type PlaceBidInput, RealtimeModule, type Sample, type SampleReason, type SignalSource, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type Wallet as UserWallet, type Wallet$1 as Wallet, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
package/dist/index.d.ts CHANGED
@@ -63,6 +63,49 @@ interface GraphQLTransport {
63
63
  request<T>(query: string, variables?: Record<string, unknown>, opts?: GraphQLRequestOpts): Promise<T>;
64
64
  }
65
65
 
66
+ type AgencyPairStatus = 'active' | 'paused' | 'ended';
67
+ interface AgencyPairFilter {
68
+ /** Exact match on the display pair name, e.g. "ETH/USD". */
69
+ pair?: string;
70
+ /** Exact match on the agency pair status. */
71
+ status?: AgencyPairStatus;
72
+ }
73
+ interface AgencyPairStats {
74
+ /** game_pairs.id slug, e.g. "grid-ETH-USD". Use to build the stats MQTT topic. */
75
+ gamePairId: string;
76
+ /** Display name, e.g. "ETH/USD". */
77
+ pair: string;
78
+ /** Icon URL. */
79
+ thumb: string;
80
+ /** Lower = earlier in the displayed list. */
81
+ ordering: number;
82
+ /** config.constraints.maxCoef. */
83
+ maxCoef: number;
84
+ /** Latest candle close price; 0 when no candle exists. */
85
+ currentPrice: number;
86
+ /** Sliding 24h bid-amount sum; 0 when the worker snapshot is missing or stale. */
87
+ currentVol24h: number;
88
+ }
89
+
90
+ interface AgencyPairModuleDeps {
91
+ graphql: GraphQLTransport;
92
+ }
93
+ declare class AgencyPairModule {
94
+ #private;
95
+ constructor(deps: AgencyPairModuleDeps);
96
+ /**
97
+ * List the agency-in-context's pairs with live derived stats.
98
+ *
99
+ * The agency is resolved server-side from the `x-builder-code` header the SDK
100
+ * sends on every request (no JWT required — Tier 0 anon-readable). There is no
101
+ * argument to target another agency by design.
102
+ */
103
+ list(args?: {
104
+ filter?: AgencyPairFilter;
105
+ signal?: AbortSignal;
106
+ }): Promise<AgencyPairStats[]>;
107
+ }
108
+
66
109
  interface RestRequestOpts {
67
110
  signal?: AbortSignal;
68
111
  }
@@ -278,8 +321,8 @@ declare class GameModule {
278
321
  }): Promise<Candle[]>;
279
322
  }
280
323
 
281
- type LeaderboardPeriod = '24h' | '7d' | '30d' | 'all';
282
- type LeaderboardSortBy = 'gain' | 'wins' | 'total_payout' | 'total_wagered' | 'total_bids';
324
+ type LeaderboardPeriod = 'weekly' | 'all_time';
325
+ type LeaderboardSortBy = 'gain' | 'wagered';
283
326
  interface LeaderboardEntry {
284
327
  userId: string;
285
328
  username: string;
@@ -433,7 +476,13 @@ interface MqttWireConfig {
433
476
  max_bid_amount: number;
434
477
  acceptable_bids: number[];
435
478
  }
436
- type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig;
479
+ interface MqttWireAgencyPairStats {
480
+ price: number;
481
+ vol24h: number;
482
+ maxCoef: number;
483
+ ts: number;
484
+ }
485
+ type MqttWirePayload = MqttWireCandle | MqttWireBidResult | MqttWireBalanceUpdate | MqttWireConfig | MqttWireAgencyPairStats;
437
486
  type MqttMessageHandler = (gameId: string, topic: string, payload: MqttWirePayload) => void;
438
487
  type MqttErrorHandler = (err: Error) => void;
439
488
  type MqttLifecycleEvent = {
@@ -453,6 +502,7 @@ type MqttLifecycleEvent = {
453
502
  };
454
503
  type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
455
504
  type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
505
+ type MqttAgencyPairStatsHandler = (topic: string, payload: MqttWirePayload) => void;
456
506
  interface MqttTransport {
457
507
  /**
458
508
  * Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
@@ -477,6 +527,15 @@ interface MqttTransport {
477
527
  subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
478
528
  /** Tear down the candle subscription for a pair. */
479
529
  unsubscribeCandle(pair: string): void;
530
+ /**
531
+ * Subscribe to the public per-pair stats stream for an agency, topic
532
+ * `public/agency/{aid}/pair/{gamePairId}/stats`. `aid` and `gamePairId` are
533
+ * used raw (no slash escaping) to byte-match the backend publisher.
534
+ * Idempotent: multiple calls for the same `(aid, gamePairId)` are deduplicated.
535
+ */
536
+ subscribeAgencyPairStats(aid: string, gamePairId: string, onStats: MqttAgencyPairStatsHandler): void;
537
+ /** Tear down the stats subscription for an `(aid, gamePairId)`. */
538
+ unsubscribeAgencyPairStats(aid: string, gamePairId: string): void;
480
539
  close(): void;
481
540
  }
482
541
 
@@ -549,6 +608,19 @@ interface MqttIdealConfigEvent {
549
608
  currentPrice: number;
550
609
  reason: string;
551
610
  }
611
+ /**
612
+ * Live per-pair stats tick from the public stream
613
+ * `public/agency/{aid}/pair/{gamePairId}/stats`. The backend suppresses
614
+ * unchanged ticks, so a subscriber may not receive an event until price or
615
+ * vol24h actually moves — seed initial values from `agencyPairs.list()`.
616
+ */
617
+ interface MqttAgencyPairStatsEvent {
618
+ price: number;
619
+ vol24h: number;
620
+ maxCoef: number;
621
+ /** Unix epoch **seconds** (matches the backend snapshot `ts`). */
622
+ ts: number;
623
+ }
552
624
 
553
625
  interface GameChannelEvents {
554
626
  candle: [MqttCandleEvent];
@@ -568,6 +640,12 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
568
640
 
569
641
  interface RealtimeModuleOptions {
570
642
  mqttEndpoint: string;
643
+ /**
644
+ * The client's own agency id. Threaded so `subscribeAgencyPairStats` can
645
+ * scope the stats topic to this agency without accepting a target agency from
646
+ * the caller (tenant-scoping mirror of the GQL resolver).
647
+ */
648
+ agencyId?: string;
571
649
  /** @internal For testing only */
572
650
  transport?: MqttTransport;
573
651
  /**
@@ -594,6 +672,14 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
594
672
  subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
595
673
  /** Unsubscribe from candle data for a pair. */
596
674
  unsubscribeCandle(pair: string): void;
675
+ /**
676
+ * Subscribe to the live per-pair stats stream for the client's own agency.
677
+ * Scoped to the agency the client was constructed with — there is no argument
678
+ * to target another agency by design.
679
+ */
680
+ subscribeAgencyPairStats(gamePairId: string, handler: (event: MqttAgencyPairStatsEvent) => void): void;
681
+ /** Unsubscribe from the per-pair stats stream for the client's own agency. */
682
+ unsubscribeAgencyPairStats(gamePairId: string): void;
597
683
  disconnect(): void;
598
684
  }
599
685
 
@@ -702,6 +788,8 @@ declare class TaphubClient {
702
788
  bid: BidModule;
703
789
  /** @readonly Leaderboard module — reassignment has no effect at runtime. */
704
790
  leaderboard: LeaderboardModule;
791
+ /** @readonly Agency pair module — reassignment has no effect at runtime. */
792
+ agencyPairs: AgencyPairModule;
705
793
  /** @readonly Locale module — reassignment has no effect at runtime. */
706
794
  locale: LocaleModule;
707
795
  /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
@@ -791,4 +879,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
791
879
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
792
880
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
793
881
 
794
- export { AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type Game, GameChannel, type GameConfig, GameModule, type GamePairInfo, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type PlaceBidInput, RealtimeModule, type Sample, type SampleReason, type SignalSource, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type Wallet as UserWallet, type Wallet$1 as Wallet, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
882
+ export { type AgencyPairFilter, AgencyPairModule, type AgencyPairStats, type AgencyPairStatus, AuthModule, type BackendHealth, type Bid, BidModule, type BidStatus, type CancelBidResult, type Candle, type Constraints, type Currency, type Game, GameChannel, type GameConfig, GameModule, type GamePairInfo, type GridConfig, type LeaderboardEntry, LeaderboardModule, type LeaderboardPeriod, type LeaderboardSortBy, LocaleModule, type LocaleRefreshResult, type LocaleResponse, type LocaleTranslations, type LoginResult, type Me, type MqttAcceptedBid, type MqttAgencyPairStatsEvent, type MqttBalanceEvent, type MqttBidAcceptedEvent, type MqttBidLostEvent, type MqttBidWonEvent, type MqttCandleEvent, type MqttConfigEvent, type MqttIdealConfigEvent, type NetworkLevel, type NetworkQuality, NetworkQualityMonitor, type PlaceBidInput, RealtimeModule, type Sample, type SampleReason, type SignalSource, TaphubAuthError, TaphubClient, type TaphubClientConfig, TaphubError, TaphubEventBus, type TaphubEventMap, TaphubNetworkError, TaphubServerError, TaphubSlippageError, TaphubStorage, type TaphubStorageAdapter, TaphubValidationError, type User, UserModule, type Wallet as UserWallet, type Wallet$1 as Wallet, adaptiveSimpson, autoDetectStorage, calculateProbWin, calculateProbWin_v2, errorFunction, isCancelled, isLoss, isPending, isTerminal, isWin, normalCDF, normalPDF };
package/dist/index.js CHANGED
@@ -66,6 +66,61 @@ var TaphubEventBus = class {
66
66
  }
67
67
  };
68
68
 
69
+ // src/modules/agencyPair/normalise.ts
70
+ function normaliseAgencyPairs(rows) {
71
+ if (!Array.isArray(rows)) {
72
+ throw new TaphubServerError("Invalid response from server", {
73
+ code: "InvalidResponse"
74
+ });
75
+ }
76
+ return rows.map((row) => ({
77
+ gamePairId: row.gamePairId,
78
+ pair: row.pair,
79
+ thumb: row.thumb,
80
+ ordering: Number(row.ordering),
81
+ maxCoef: Number(row.maxCoef),
82
+ currentPrice: Number(row.currentPrice),
83
+ currentVol24h: Number(row.currentVol24h)
84
+ }));
85
+ }
86
+
87
+ // src/modules/agencyPair/queries.ts
88
+ var FETCH_AGENCY_PAIRS_QUERY = `query FetchAgencyPairs($filter: AgencyPairFilter) {
89
+ fetchAgencyPairs(filter: $filter) {
90
+ gamePairId pair thumb ordering maxCoef currentPrice currentVol24h
91
+ }
92
+ }`;
93
+
94
+ // src/modules/agencyPair/index.ts
95
+ var AgencyPairModule = class {
96
+ #graphql;
97
+ constructor(deps) {
98
+ this.#graphql = deps.graphql;
99
+ }
100
+ /**
101
+ * List the agency-in-context's pairs with live derived stats.
102
+ *
103
+ * The agency is resolved server-side from the `x-builder-code` header the SDK
104
+ * sends on every request (no JWT required — Tier 0 anon-readable). There is no
105
+ * argument to target another agency by design.
106
+ */
107
+ async list(args) {
108
+ const variables = {};
109
+ if (args?.filter) {
110
+ const filter = {};
111
+ if (args.filter.pair !== void 0) filter.pair = args.filter.pair;
112
+ if (args.filter.status !== void 0) filter.status = args.filter.status;
113
+ variables.filter = filter;
114
+ }
115
+ const body = await this.#graphql.request(
116
+ FETCH_AGENCY_PAIRS_QUERY,
117
+ variables,
118
+ args?.signal ? { signal: args.signal } : void 0
119
+ );
120
+ return normaliseAgencyPairs(body.fetchAgencyPairs);
121
+ }
122
+ };
123
+
69
124
  // src/modules/auth/normalise.ts
70
125
  function normaliseGoogleResponse(body) {
71
126
  return {
@@ -765,6 +820,9 @@ var GAME_SCOPED_SUFFIXES = ["config", "ideal_config"];
765
820
  var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
766
821
  var TOPIC_PREFIX = "game";
767
822
  var MARKET_TOPIC_PREFIX = "market";
823
+ function agencyPairStatsTopic(aid, gamePairId) {
824
+ return `public/agency/${aid}/pair/${gamePairId}/stats`;
825
+ }
768
826
  function topicFor(gameId, suffix, userId) {
769
827
  if (USER_SCOPED_SUFFIXES.includes(suffix)) {
770
828
  return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
@@ -781,6 +839,7 @@ function createMqttTransport(endpoint, opts = {}) {
781
839
  let client = null;
782
840
  const subscriptions = /* @__PURE__ */ new Map();
783
841
  const candleSubscriptions = /* @__PURE__ */ new Map();
842
+ const statsSubscriptions = /* @__PURE__ */ new Map();
784
843
  const { onLifecycle } = opts;
785
844
  let connectStartedAt = 0;
786
845
  function fireLifecycle(event) {
@@ -838,6 +897,17 @@ function createMqttTransport(endpoint, opts = {}) {
838
897
  candleSub.onCandle(receivedTopic, payload2);
839
898
  return;
840
899
  }
900
+ const statsSub = statsSubscriptions.get(receivedTopic);
901
+ if (statsSub) {
902
+ let payload2;
903
+ try {
904
+ payload2 = JSON.parse(message.toString());
905
+ } catch {
906
+ return;
907
+ }
908
+ statsSub.onStats(receivedTopic, payload2);
909
+ return;
910
+ }
841
911
  let matched;
842
912
  for (const sub of subscriptions.values()) {
843
913
  if (sub.topics.includes(receivedTopic)) {
@@ -898,6 +968,20 @@ function createMqttTransport(endpoint, opts = {}) {
898
968
  if (client) client.unsubscribe(entry.topic);
899
969
  candleSubscriptions.delete(pair);
900
970
  },
971
+ subscribeAgencyPairStats(aid, gamePairId, onStats) {
972
+ const topic = agencyPairStatsTopic(aid, gamePairId);
973
+ if (statsSubscriptions.has(topic)) return;
974
+ const mqttClient = ensureConnected();
975
+ statsSubscriptions.set(topic, { topic, onStats });
976
+ mqttClient.subscribe(topic, { qos: 0 });
977
+ },
978
+ unsubscribeAgencyPairStats(aid, gamePairId) {
979
+ const topic = agencyPairStatsTopic(aid, gamePairId);
980
+ const entry = statsSubscriptions.get(topic);
981
+ if (!entry) return;
982
+ if (client) client.unsubscribe(entry.topic);
983
+ statsSubscriptions.delete(topic);
984
+ },
901
985
  unsubscribeAll(gameId, userId) {
902
986
  const matches = entriesForGame(gameId);
903
987
  if (matches.length === 0) return;
@@ -932,9 +1016,13 @@ function createMqttTransport(endpoint, opts = {}) {
932
1016
  for (const entry of candleSubscriptions.values()) {
933
1017
  client.unsubscribe(entry.topic);
934
1018
  }
1019
+ for (const entry of statsSubscriptions.values()) {
1020
+ client.unsubscribe(entry.topic);
1021
+ }
935
1022
  }
936
1023
  subscriptions.clear();
937
1024
  candleSubscriptions.clear();
1025
+ statsSubscriptions.clear();
938
1026
  if (client) {
939
1027
  client.end(true);
940
1028
  client = null;
@@ -1055,11 +1143,13 @@ function normaliseUserId2(userId) {
1055
1143
  var RealtimeModule = class extends EventEmitter2 {
1056
1144
  #transport;
1057
1145
  #entries = /* @__PURE__ */ new Map();
1146
+ #agencyId;
1058
1147
  constructor(mqttEndpointOrOptions) {
1059
1148
  super();
1060
1149
  if (typeof mqttEndpointOrOptions === "string") {
1061
1150
  this.#transport = createMqttTransport(mqttEndpointOrOptions);
1062
1151
  } else {
1152
+ this.#agencyId = mqttEndpointOrOptions.agencyId;
1063
1153
  this.#transport = mqttEndpointOrOptions.transport ?? createMqttTransport(mqttEndpointOrOptions.mqttEndpoint, {
1064
1154
  onLifecycle: mqttEndpointOrOptions.onMqttLifecycle
1065
1155
  });
@@ -1143,6 +1233,33 @@ var RealtimeModule = class extends EventEmitter2 {
1143
1233
  unsubscribeCandle(pair) {
1144
1234
  this.#transport.unsubscribeCandle(pair);
1145
1235
  }
1236
+ /**
1237
+ * Subscribe to the live per-pair stats stream for the client's own agency.
1238
+ * Scoped to the agency the client was constructed with — there is no argument
1239
+ * to target another agency by design.
1240
+ */
1241
+ subscribeAgencyPairStats(gamePairId, handler) {
1242
+ if (!this.#agencyId) {
1243
+ throw new TaphubError("agencyId is required to subscribe to agency pair stats", {
1244
+ code: "AgencyIdRequired"
1245
+ });
1246
+ }
1247
+ const onStats = (_topic, raw) => {
1248
+ const p = raw;
1249
+ handler({
1250
+ price: Number(p.price),
1251
+ vol24h: Number(p.vol24h),
1252
+ maxCoef: Number(p.maxCoef),
1253
+ ts: Number(p.ts)
1254
+ });
1255
+ };
1256
+ this.#transport.subscribeAgencyPairStats(this.#agencyId, gamePairId, onStats);
1257
+ }
1258
+ /** Unsubscribe from the per-pair stats stream for the client's own agency. */
1259
+ unsubscribeAgencyPairStats(gamePairId) {
1260
+ if (!this.#agencyId) return;
1261
+ this.#transport.unsubscribeAgencyPairStats(this.#agencyId, gamePairId);
1262
+ }
1146
1263
  disconnect() {
1147
1264
  for (const entry of this.#entries.values()) {
1148
1265
  entry.channel.removeAllListeners();
@@ -2207,6 +2324,8 @@ var TaphubClient = class {
2207
2324
  bid;
2208
2325
  /** @readonly Leaderboard module — reassignment has no effect at runtime. */
2209
2326
  leaderboard;
2327
+ /** @readonly Agency pair module — reassignment has no effect at runtime. */
2328
+ agencyPairs;
2210
2329
  /** @readonly Locale module — reassignment has no effect at runtime. */
2211
2330
  locale;
2212
2331
  /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
@@ -2287,9 +2406,11 @@ var TaphubClient = class {
2287
2406
  this.game = new GameModule({ graphql: this.#graphql });
2288
2407
  this.bid = new BidModule({ graphql: this.#graphql });
2289
2408
  this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
2409
+ this.agencyPairs = new AgencyPairModule({ graphql: this.#graphql });
2290
2410
  this.locale = new LocaleModule({ graphql: this.#graphql });
2291
2411
  this.realtime = config.mqttEndpoint ? new RealtimeModule({
2292
2412
  mqttEndpoint: config.mqttEndpoint,
2413
+ agencyId: this.agencyId,
2293
2414
  onMqttLifecycle: createMqttProbe(this.network)
2294
2415
  }) : void 0;
2295
2416
  attachConnectionProbe(this.network);
@@ -2323,6 +2444,12 @@ var TaphubClient = class {
2323
2444
  enumerable: true,
2324
2445
  configurable: false
2325
2446
  });
2447
+ Object.defineProperty(this, "agencyPairs", {
2448
+ value: this.agencyPairs,
2449
+ writable: false,
2450
+ enumerable: true,
2451
+ configurable: false
2452
+ });
2326
2453
  if (this.realtime) {
2327
2454
  Object.defineProperty(this, "realtime", {
2328
2455
  value: this.realtime,
@@ -2458,6 +2585,7 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
2458
2585
  return Math.max(0, Math.min(1, prob));
2459
2586
  }
2460
2587
  export {
2588
+ AgencyPairModule,
2461
2589
  AuthModule,
2462
2590
  BidModule,
2463
2591
  GameModule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taphubhq/sdk-core",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Core SDK for building on the TabHub platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",