@taphubhq/sdk-core 0.13.0 → 0.14.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/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 {
@@ -818,9 +874,20 @@ var LocaleModule = class {
818
874
 
819
875
  // src/transport/mqtt/index.ts
820
876
  var import_mqtt = __toESM(require("mqtt"));
821
- var GAME_SCOPED_SUFFIXES = ["candle", "config", "ideal_config"];
877
+
878
+ // src/transport/mqtt/utils.ts
879
+ function subKey(gameId, userId) {
880
+ return `${gameId}::${userId ?? ""}`;
881
+ }
882
+
883
+ // src/transport/mqtt/index.ts
884
+ var GAME_SCOPED_SUFFIXES = ["config", "ideal_config"];
822
885
  var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
823
886
  var TOPIC_PREFIX = "game";
887
+ var MARKET_TOPIC_PREFIX = "market";
888
+ function agencyPairStatsTopic(aid, gamePairId) {
889
+ return `public/agency/${aid}/pair/${gamePairId}/stats`;
890
+ }
824
891
  function topicFor(gameId, suffix, userId) {
825
892
  if (USER_SCOPED_SUFFIXES.includes(suffix)) {
826
893
  return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
@@ -833,12 +900,11 @@ function normaliseUserId(userId) {
833
900
  function userScopedTopicsFor(gameId, userId) {
834
901
  return USER_SCOPED_SUFFIXES.map((s) => topicFor(gameId, s, userId));
835
902
  }
836
- function subKey(gameId, userId) {
837
- return `${gameId}::${userId ?? ""}`;
838
- }
839
903
  function createMqttTransport(endpoint, opts = {}) {
840
904
  let client = null;
841
905
  const subscriptions = /* @__PURE__ */ new Map();
906
+ const candleSubscriptions = /* @__PURE__ */ new Map();
907
+ const statsSubscriptions = /* @__PURE__ */ new Map();
842
908
  const { onLifecycle } = opts;
843
909
  let connectStartedAt = 0;
844
910
  function fireLifecycle(event) {
@@ -885,6 +951,28 @@ function createMqttTransport(endpoint, opts = {}) {
885
951
  fireLifecycle({ kind: "error", err });
886
952
  });
887
953
  client.on("message", (receivedTopic, message) => {
954
+ const candleSub = [...candleSubscriptions.values()].find((s) => s.topic === receivedTopic);
955
+ if (candleSub) {
956
+ let payload2;
957
+ try {
958
+ payload2 = JSON.parse(message.toString());
959
+ } catch {
960
+ return;
961
+ }
962
+ candleSub.onCandle(receivedTopic, payload2);
963
+ return;
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
+ }
888
976
  let matched;
889
977
  for (const sub of subscriptions.values()) {
890
978
  if (sub.topics.includes(receivedTopic)) {
@@ -929,9 +1017,36 @@ function createMqttTransport(endpoint, opts = {}) {
929
1017
  onError
930
1018
  });
931
1019
  for (const t of fullTopics) {
932
- mqttClient.subscribe(t, { qos: t.endsWith("candle") ? 0 : 1 });
1020
+ mqttClient.subscribe(t, { qos: 1 });
933
1021
  }
934
1022
  },
1023
+ subscribeCandle(pair, onCandle) {
1024
+ const topic = `${MARKET_TOPIC_PREFIX}/${pair.replace(/\//g, "_")}/candle`;
1025
+ if (candleSubscriptions.has(pair)) return;
1026
+ const mqttClient = ensureConnected();
1027
+ candleSubscriptions.set(pair, { topic, onCandle });
1028
+ mqttClient.subscribe(topic, { qos: 0 });
1029
+ },
1030
+ unsubscribeCandle(pair) {
1031
+ const entry = candleSubscriptions.get(pair);
1032
+ if (!entry) return;
1033
+ if (client) client.unsubscribe(entry.topic);
1034
+ candleSubscriptions.delete(pair);
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
+ },
935
1050
  unsubscribeAll(gameId, userId) {
936
1051
  const matches = entriesForGame(gameId);
937
1052
  if (matches.length === 0) return;
@@ -963,8 +1078,16 @@ function createMqttTransport(endpoint, opts = {}) {
963
1078
  client.unsubscribe(t);
964
1079
  }
965
1080
  }
1081
+ for (const entry of candleSubscriptions.values()) {
1082
+ client.unsubscribe(entry.topic);
1083
+ }
1084
+ for (const entry of statsSubscriptions.values()) {
1085
+ client.unsubscribe(entry.topic);
1086
+ }
966
1087
  }
967
1088
  subscriptions.clear();
1089
+ candleSubscriptions.clear();
1090
+ statsSubscriptions.clear();
968
1091
  if (client) {
969
1092
  client.end(true);
970
1093
  client = null;
@@ -1082,17 +1205,16 @@ function mapWireToEvent(topic, payload) {
1082
1205
  function normaliseUserId2(userId) {
1083
1206
  return userId && userId !== "" ? userId : null;
1084
1207
  }
1085
- function subKey2(gameId, userId) {
1086
- return `${gameId}::${userId ?? ""}`;
1087
- }
1088
1208
  var RealtimeModule = class extends import_eventemitter32.default {
1089
1209
  #transport;
1090
1210
  #entries = /* @__PURE__ */ new Map();
1211
+ #agencyId;
1091
1212
  constructor(mqttEndpointOrOptions) {
1092
1213
  super();
1093
1214
  if (typeof mqttEndpointOrOptions === "string") {
1094
1215
  this.#transport = createMqttTransport(mqttEndpointOrOptions);
1095
1216
  } else {
1217
+ this.#agencyId = mqttEndpointOrOptions.agencyId;
1096
1218
  this.#transport = mqttEndpointOrOptions.transport ?? createMqttTransport(mqttEndpointOrOptions.mqttEndpoint, {
1097
1219
  onLifecycle: mqttEndpointOrOptions.onMqttLifecycle
1098
1220
  });
@@ -1104,7 +1226,7 @@ var RealtimeModule = class extends import_eventemitter32.default {
1104
1226
  }
1105
1227
  subscribe(gameId, userId) {
1106
1228
  const cleanUserId = normaliseUserId2(userId);
1107
- const key = subKey2(gameId, cleanUserId);
1229
+ const key = subKey(gameId, cleanUserId);
1108
1230
  const existing = this.#entries.get(key);
1109
1231
  if (existing) {
1110
1232
  existing.refcount += 1;
@@ -1160,11 +1282,49 @@ var RealtimeModule = class extends import_eventemitter32.default {
1160
1282
  }
1161
1283
  target.refcount -= 1;
1162
1284
  if (target.refcount > 0) return;
1163
- const key = subKey2(target.gameId, target.userId);
1285
+ const key = subKey(target.gameId, target.userId);
1164
1286
  this.#entries.delete(key);
1165
1287
  target.channel.removeAllListeners();
1166
1288
  this.#transport.unsubscribeAll(gameId, target.userId);
1167
1289
  }
1290
+ /**
1291
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
1292
+ * Independent of game/agency — candle is market-wide public data.
1293
+ */
1294
+ subscribeCandle(pair, onCandle) {
1295
+ this.#transport.subscribeCandle(pair, onCandle);
1296
+ }
1297
+ /** Unsubscribe from candle data for a pair. */
1298
+ unsubscribeCandle(pair) {
1299
+ this.#transport.unsubscribeCandle(pair);
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
+ }
1168
1328
  disconnect() {
1169
1329
  for (const entry of this.#entries.values()) {
1170
1330
  entry.channel.removeAllListeners();
@@ -2229,6 +2389,8 @@ var TaphubClient = class {
2229
2389
  bid;
2230
2390
  /** @readonly Leaderboard module — reassignment has no effect at runtime. */
2231
2391
  leaderboard;
2392
+ /** @readonly Agency pair module — reassignment has no effect at runtime. */
2393
+ agencyPairs;
2232
2394
  /** @readonly Locale module — reassignment has no effect at runtime. */
2233
2395
  locale;
2234
2396
  /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
@@ -2309,9 +2471,11 @@ var TaphubClient = class {
2309
2471
  this.game = new GameModule({ graphql: this.#graphql });
2310
2472
  this.bid = new BidModule({ graphql: this.#graphql });
2311
2473
  this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
2474
+ this.agencyPairs = new AgencyPairModule({ graphql: this.#graphql });
2312
2475
  this.locale = new LocaleModule({ graphql: this.#graphql });
2313
2476
  this.realtime = config.mqttEndpoint ? new RealtimeModule({
2314
2477
  mqttEndpoint: config.mqttEndpoint,
2478
+ agencyId: this.agencyId,
2315
2479
  onMqttLifecycle: createMqttProbe(this.network)
2316
2480
  }) : void 0;
2317
2481
  attachConnectionProbe(this.network);
@@ -2345,6 +2509,12 @@ var TaphubClient = class {
2345
2509
  enumerable: true,
2346
2510
  configurable: false
2347
2511
  });
2512
+ Object.defineProperty(this, "agencyPairs", {
2513
+ value: this.agencyPairs,
2514
+ writable: false,
2515
+ enumerable: true,
2516
+ configurable: false
2517
+ });
2348
2518
  if (this.realtime) {
2349
2519
  Object.defineProperty(this, "realtime", {
2350
2520
  value: this.realtime,
@@ -2481,6 +2651,7 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
2481
2651
  }
2482
2652
  // Annotate the CommonJS export names for ESM import in node:
2483
2653
  0 && (module.exports = {
2654
+ AgencyPairModule,
2484
2655
  AuthModule,
2485
2656
  BidModule,
2486
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
  }
@@ -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 = {
@@ -452,14 +501,14 @@ type MqttLifecycleEvent = {
452
501
  rttMs: number;
453
502
  };
454
503
  type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
504
+ type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
505
+ type MqttAgencyPairStatsHandler = (topic: string, payload: MqttWirePayload) => void;
455
506
  interface MqttTransport {
456
507
  /**
457
- * Subscribe to a game's MQTT topics keyed by the tuple `(gameId, userId)`.
458
- * When `userId` is nullish (including the empty string `''`), only
459
- * game-scoped topics are subscribed and the entry is stored under the
460
- * anonymous slot for that gameId. Two calls with the same `(gameId, userId)`
461
- * tuple are idempotent; calls with the same `gameId` but a different
462
- * `userId` create an independent entry with its own user-scoped topics.
508
+ * Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
509
+ * keyed by the tuple `(gameId, userId)`. Does NOT subscribe to candle —
510
+ * use `subscribeCandle` for public market candle data.
511
+ * Two calls with the same `(gameId, userId)` tuple are idempotent.
463
512
  */
464
513
  subscribe(gameId: string, userId: string | null | undefined, onMessage: MqttMessageHandler, onError: MqttErrorHandler): void;
465
514
  /**
@@ -470,6 +519,23 @@ interface MqttTransport {
470
519
  * `code='AmbiguousUnsubscribe'`.
471
520
  */
472
521
  unsubscribeAll(gameId: string, userId?: string | null): void;
522
+ /**
523
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
524
+ * Independent of game/agency context — candle is market-wide public data.
525
+ * Idempotent: multiple calls for the same pair are deduplicated.
526
+ */
527
+ subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
528
+ /** Tear down the candle subscription for a pair. */
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;
473
539
  close(): void;
474
540
  }
475
541
 
@@ -542,6 +608,19 @@ interface MqttIdealConfigEvent {
542
608
  currentPrice: number;
543
609
  reason: string;
544
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
+ }
545
624
 
546
625
  interface GameChannelEvents {
547
626
  candle: [MqttCandleEvent];
@@ -561,6 +640,12 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
561
640
 
562
641
  interface RealtimeModuleOptions {
563
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;
564
649
  /** @internal For testing only */
565
650
  transport?: MqttTransport;
566
651
  /**
@@ -580,6 +665,21 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
580
665
  get _transport(): MqttTransport;
581
666
  subscribe(gameId: string, userId?: string | null): GameChannel;
582
667
  unsubscribe(gameId: string, userId?: string | null): void;
668
+ /**
669
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
670
+ * Independent of game/agency — candle is market-wide public data.
671
+ */
672
+ subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
673
+ /** Unsubscribe from candle data for a pair. */
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;
583
683
  disconnect(): void;
584
684
  }
585
685
 
@@ -688,6 +788,8 @@ declare class TaphubClient {
688
788
  bid: BidModule;
689
789
  /** @readonly Leaderboard module — reassignment has no effect at runtime. */
690
790
  leaderboard: LeaderboardModule;
791
+ /** @readonly Agency pair module — reassignment has no effect at runtime. */
792
+ agencyPairs: AgencyPairModule;
691
793
  /** @readonly Locale module — reassignment has no effect at runtime. */
692
794
  locale: LocaleModule;
693
795
  /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
@@ -777,4 +879,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
777
879
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
778
880
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
779
881
 
780
- 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
  }
@@ -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 = {
@@ -452,14 +501,14 @@ type MqttLifecycleEvent = {
452
501
  rttMs: number;
453
502
  };
454
503
  type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
504
+ type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
505
+ type MqttAgencyPairStatsHandler = (topic: string, payload: MqttWirePayload) => void;
455
506
  interface MqttTransport {
456
507
  /**
457
- * Subscribe to a game's MQTT topics keyed by the tuple `(gameId, userId)`.
458
- * When `userId` is nullish (including the empty string `''`), only
459
- * game-scoped topics are subscribed and the entry is stored under the
460
- * anonymous slot for that gameId. Two calls with the same `(gameId, userId)`
461
- * tuple are idempotent; calls with the same `gameId` but a different
462
- * `userId` create an independent entry with its own user-scoped topics.
508
+ * Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
509
+ * keyed by the tuple `(gameId, userId)`. Does NOT subscribe to candle —
510
+ * use `subscribeCandle` for public market candle data.
511
+ * Two calls with the same `(gameId, userId)` tuple are idempotent.
463
512
  */
464
513
  subscribe(gameId: string, userId: string | null | undefined, onMessage: MqttMessageHandler, onError: MqttErrorHandler): void;
465
514
  /**
@@ -470,6 +519,23 @@ interface MqttTransport {
470
519
  * `code='AmbiguousUnsubscribe'`.
471
520
  */
472
521
  unsubscribeAll(gameId: string, userId?: string | null): void;
522
+ /**
523
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
524
+ * Independent of game/agency context — candle is market-wide public data.
525
+ * Idempotent: multiple calls for the same pair are deduplicated.
526
+ */
527
+ subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
528
+ /** Tear down the candle subscription for a pair. */
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;
473
539
  close(): void;
474
540
  }
475
541
 
@@ -542,6 +608,19 @@ interface MqttIdealConfigEvent {
542
608
  currentPrice: number;
543
609
  reason: string;
544
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
+ }
545
624
 
546
625
  interface GameChannelEvents {
547
626
  candle: [MqttCandleEvent];
@@ -561,6 +640,12 @@ declare class GameChannel extends EventEmitter<GameChannelEvents> {
561
640
 
562
641
  interface RealtimeModuleOptions {
563
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;
564
649
  /** @internal For testing only */
565
650
  transport?: MqttTransport;
566
651
  /**
@@ -580,6 +665,21 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
580
665
  get _transport(): MqttTransport;
581
666
  subscribe(gameId: string, userId?: string | null): GameChannel;
582
667
  unsubscribe(gameId: string, userId?: string | null): void;
668
+ /**
669
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
670
+ * Independent of game/agency — candle is market-wide public data.
671
+ */
672
+ subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
673
+ /** Unsubscribe from candle data for a pair. */
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;
583
683
  disconnect(): void;
584
684
  }
585
685
 
@@ -688,6 +788,8 @@ declare class TaphubClient {
688
788
  bid: BidModule;
689
789
  /** @readonly Leaderboard module — reassignment has no effect at runtime. */
690
790
  leaderboard: LeaderboardModule;
791
+ /** @readonly Agency pair module — reassignment has no effect at runtime. */
792
+ agencyPairs: AgencyPairModule;
691
793
  /** @readonly Locale module — reassignment has no effect at runtime. */
692
794
  locale: LocaleModule;
693
795
  /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
@@ -777,4 +879,4 @@ declare function adaptiveSimpson(f: (x: number) => number, a: number, b: number,
777
879
  declare function calculateProbWin(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
778
880
  declare function calculateProbWin_v2(time1: number, time2: number, _price1: number, price2: number, creationTime: number, currentPrice: number, volatility: number): number;
779
881
 
780
- 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 {
@@ -754,9 +809,20 @@ var LocaleModule = class {
754
809
 
755
810
  // src/transport/mqtt/index.ts
756
811
  import mqtt from "mqtt";
757
- var GAME_SCOPED_SUFFIXES = ["candle", "config", "ideal_config"];
812
+
813
+ // src/transport/mqtt/utils.ts
814
+ function subKey(gameId, userId) {
815
+ return `${gameId}::${userId ?? ""}`;
816
+ }
817
+
818
+ // src/transport/mqtt/index.ts
819
+ var GAME_SCOPED_SUFFIXES = ["config", "ideal_config"];
758
820
  var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
759
821
  var TOPIC_PREFIX = "game";
822
+ var MARKET_TOPIC_PREFIX = "market";
823
+ function agencyPairStatsTopic(aid, gamePairId) {
824
+ return `public/agency/${aid}/pair/${gamePairId}/stats`;
825
+ }
760
826
  function topicFor(gameId, suffix, userId) {
761
827
  if (USER_SCOPED_SUFFIXES.includes(suffix)) {
762
828
  return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
@@ -769,12 +835,11 @@ function normaliseUserId(userId) {
769
835
  function userScopedTopicsFor(gameId, userId) {
770
836
  return USER_SCOPED_SUFFIXES.map((s) => topicFor(gameId, s, userId));
771
837
  }
772
- function subKey(gameId, userId) {
773
- return `${gameId}::${userId ?? ""}`;
774
- }
775
838
  function createMqttTransport(endpoint, opts = {}) {
776
839
  let client = null;
777
840
  const subscriptions = /* @__PURE__ */ new Map();
841
+ const candleSubscriptions = /* @__PURE__ */ new Map();
842
+ const statsSubscriptions = /* @__PURE__ */ new Map();
778
843
  const { onLifecycle } = opts;
779
844
  let connectStartedAt = 0;
780
845
  function fireLifecycle(event) {
@@ -821,6 +886,28 @@ function createMqttTransport(endpoint, opts = {}) {
821
886
  fireLifecycle({ kind: "error", err });
822
887
  });
823
888
  client.on("message", (receivedTopic, message) => {
889
+ const candleSub = [...candleSubscriptions.values()].find((s) => s.topic === receivedTopic);
890
+ if (candleSub) {
891
+ let payload2;
892
+ try {
893
+ payload2 = JSON.parse(message.toString());
894
+ } catch {
895
+ return;
896
+ }
897
+ candleSub.onCandle(receivedTopic, payload2);
898
+ return;
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
+ }
824
911
  let matched;
825
912
  for (const sub of subscriptions.values()) {
826
913
  if (sub.topics.includes(receivedTopic)) {
@@ -865,9 +952,36 @@ function createMqttTransport(endpoint, opts = {}) {
865
952
  onError
866
953
  });
867
954
  for (const t of fullTopics) {
868
- mqttClient.subscribe(t, { qos: t.endsWith("candle") ? 0 : 1 });
955
+ mqttClient.subscribe(t, { qos: 1 });
869
956
  }
870
957
  },
958
+ subscribeCandle(pair, onCandle) {
959
+ const topic = `${MARKET_TOPIC_PREFIX}/${pair.replace(/\//g, "_")}/candle`;
960
+ if (candleSubscriptions.has(pair)) return;
961
+ const mqttClient = ensureConnected();
962
+ candleSubscriptions.set(pair, { topic, onCandle });
963
+ mqttClient.subscribe(topic, { qos: 0 });
964
+ },
965
+ unsubscribeCandle(pair) {
966
+ const entry = candleSubscriptions.get(pair);
967
+ if (!entry) return;
968
+ if (client) client.unsubscribe(entry.topic);
969
+ candleSubscriptions.delete(pair);
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
+ },
871
985
  unsubscribeAll(gameId, userId) {
872
986
  const matches = entriesForGame(gameId);
873
987
  if (matches.length === 0) return;
@@ -899,8 +1013,16 @@ function createMqttTransport(endpoint, opts = {}) {
899
1013
  client.unsubscribe(t);
900
1014
  }
901
1015
  }
1016
+ for (const entry of candleSubscriptions.values()) {
1017
+ client.unsubscribe(entry.topic);
1018
+ }
1019
+ for (const entry of statsSubscriptions.values()) {
1020
+ client.unsubscribe(entry.topic);
1021
+ }
902
1022
  }
903
1023
  subscriptions.clear();
1024
+ candleSubscriptions.clear();
1025
+ statsSubscriptions.clear();
904
1026
  if (client) {
905
1027
  client.end(true);
906
1028
  client = null;
@@ -1018,17 +1140,16 @@ function mapWireToEvent(topic, payload) {
1018
1140
  function normaliseUserId2(userId) {
1019
1141
  return userId && userId !== "" ? userId : null;
1020
1142
  }
1021
- function subKey2(gameId, userId) {
1022
- return `${gameId}::${userId ?? ""}`;
1023
- }
1024
1143
  var RealtimeModule = class extends EventEmitter2 {
1025
1144
  #transport;
1026
1145
  #entries = /* @__PURE__ */ new Map();
1146
+ #agencyId;
1027
1147
  constructor(mqttEndpointOrOptions) {
1028
1148
  super();
1029
1149
  if (typeof mqttEndpointOrOptions === "string") {
1030
1150
  this.#transport = createMqttTransport(mqttEndpointOrOptions);
1031
1151
  } else {
1152
+ this.#agencyId = mqttEndpointOrOptions.agencyId;
1032
1153
  this.#transport = mqttEndpointOrOptions.transport ?? createMqttTransport(mqttEndpointOrOptions.mqttEndpoint, {
1033
1154
  onLifecycle: mqttEndpointOrOptions.onMqttLifecycle
1034
1155
  });
@@ -1040,7 +1161,7 @@ var RealtimeModule = class extends EventEmitter2 {
1040
1161
  }
1041
1162
  subscribe(gameId, userId) {
1042
1163
  const cleanUserId = normaliseUserId2(userId);
1043
- const key = subKey2(gameId, cleanUserId);
1164
+ const key = subKey(gameId, cleanUserId);
1044
1165
  const existing = this.#entries.get(key);
1045
1166
  if (existing) {
1046
1167
  existing.refcount += 1;
@@ -1096,11 +1217,49 @@ var RealtimeModule = class extends EventEmitter2 {
1096
1217
  }
1097
1218
  target.refcount -= 1;
1098
1219
  if (target.refcount > 0) return;
1099
- const key = subKey2(target.gameId, target.userId);
1220
+ const key = subKey(target.gameId, target.userId);
1100
1221
  this.#entries.delete(key);
1101
1222
  target.channel.removeAllListeners();
1102
1223
  this.#transport.unsubscribeAll(gameId, target.userId);
1103
1224
  }
1225
+ /**
1226
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
1227
+ * Independent of game/agency — candle is market-wide public data.
1228
+ */
1229
+ subscribeCandle(pair, onCandle) {
1230
+ this.#transport.subscribeCandle(pair, onCandle);
1231
+ }
1232
+ /** Unsubscribe from candle data for a pair. */
1233
+ unsubscribeCandle(pair) {
1234
+ this.#transport.unsubscribeCandle(pair);
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
+ }
1104
1263
  disconnect() {
1105
1264
  for (const entry of this.#entries.values()) {
1106
1265
  entry.channel.removeAllListeners();
@@ -2165,6 +2324,8 @@ var TaphubClient = class {
2165
2324
  bid;
2166
2325
  /** @readonly Leaderboard module — reassignment has no effect at runtime. */
2167
2326
  leaderboard;
2327
+ /** @readonly Agency pair module — reassignment has no effect at runtime. */
2328
+ agencyPairs;
2168
2329
  /** @readonly Locale module — reassignment has no effect at runtime. */
2169
2330
  locale;
2170
2331
  /** @readonly Realtime module — undefined when mqttEndpoint not configured. */
@@ -2245,9 +2406,11 @@ var TaphubClient = class {
2245
2406
  this.game = new GameModule({ graphql: this.#graphql });
2246
2407
  this.bid = new BidModule({ graphql: this.#graphql });
2247
2408
  this.leaderboard = new LeaderboardModule({ graphql: this.#graphql });
2409
+ this.agencyPairs = new AgencyPairModule({ graphql: this.#graphql });
2248
2410
  this.locale = new LocaleModule({ graphql: this.#graphql });
2249
2411
  this.realtime = config.mqttEndpoint ? new RealtimeModule({
2250
2412
  mqttEndpoint: config.mqttEndpoint,
2413
+ agencyId: this.agencyId,
2251
2414
  onMqttLifecycle: createMqttProbe(this.network)
2252
2415
  }) : void 0;
2253
2416
  attachConnectionProbe(this.network);
@@ -2281,6 +2444,12 @@ var TaphubClient = class {
2281
2444
  enumerable: true,
2282
2445
  configurable: false
2283
2446
  });
2447
+ Object.defineProperty(this, "agencyPairs", {
2448
+ value: this.agencyPairs,
2449
+ writable: false,
2450
+ enumerable: true,
2451
+ configurable: false
2452
+ });
2284
2453
  if (this.realtime) {
2285
2454
  Object.defineProperty(this, "realtime", {
2286
2455
  value: this.realtime,
@@ -2416,6 +2585,7 @@ function calculateProbWin_v2(time1, time2, _price1, price2, creationTime, curren
2416
2585
  return Math.max(0, Math.min(1, prob));
2417
2586
  }
2418
2587
  export {
2588
+ AgencyPairModule,
2419
2589
  AuthModule,
2420
2590
  BidModule,
2421
2591
  GameModule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taphubhq/sdk-core",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "Core SDK for building on the TabHub platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",