@@ -42,15 +42,23 @@ export interface KalshiRawMarket {
4242export interface KalshiRawEvent {
4343 event_ticker : string ;
4444 title : string ;
45+ sub_title ?: string ;
4546 image_url ?: string ;
4647 category ?: string ;
4748 tags ?: string [ ] ;
4849 series_ticker ?: string ;
50+ series_title ?: string ;
51+ mutually_exclusive ?: boolean ;
4952 markets ?: KalshiRawMarket [ ] ;
5053
5154 [ key : string ] : unknown ;
5255}
5356
57+ interface KalshiSeriesInfo {
58+ title ?: string ;
59+ tags ?: string [ ] ;
60+ }
61+
5462export interface KalshiRawEventPage {
5563 events : KalshiRawEvent [ ] ;
5664 cursor ?: string | null ;
@@ -149,7 +157,7 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
149157
150158 // Instance-level cache (moved from module-level)
151159 private cachedEvents : KalshiRawEvent [ ] | null = null ;
152- private cachedSeriesMap : Map < string , string [ ] > | null = null ;
160+ private cachedSeriesMap : Map < string , KalshiSeriesInfo > | null = null ;
153161 private lastCacheTime : number = 0 ;
154162
155163 constructor ( ctx : FetcherContext ) {
@@ -205,16 +213,16 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
205213 this . fetchAllWithStatus ( 'closed' ) ,
206214 this . fetchAllWithStatus ( 'settled' ) ,
207215 ] ) ;
208- return [ ...openEvents , ...closedEvents , ...settledEvents ] ;
216+ return this . enrichEventsWithSeriesList ( [ ...openEvents , ...closedEvents , ...settledEvents ] ) ;
209217 } else if ( status === 'closed' || status === 'inactive' ) {
210218 const [ closedEvents , settledEvents ] = await Promise . all ( [
211219 this . fetchAllWithStatus ( 'closed' ) ,
212220 this . fetchAllWithStatus ( 'settled' ) ,
213221 ] ) ;
214- return [ ...closedEvents , ...settledEvents ] ;
222+ return this . enrichEventsWithSeriesList ( [ ...closedEvents , ...settledEvents ] ) ;
215223 }
216224
217- return this . fetchAllWithStatus ( 'open' ) ;
225+ return this . enrichEventsWithSeriesList ( await this . fetchAllWithStatus ( 'open' ) ) ;
218226 } catch ( error : any ) {
219227 throw kalshiErrorMapper . mapError ( error ) ;
220228 }
@@ -232,7 +240,9 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
232240 if ( status === 'settled' ) apiStatus = 'settled' ;
233241
234242 const limit = Math . max ( 1 , Math . floor ( params . limit || BATCH_SIZE ) ) ;
235- return this . fetchPageWithStatus ( apiStatus , limit , params . cursor ) ;
243+ const page = await this . fetchPageWithStatus ( apiStatus , limit , params . cursor ) ;
244+ await this . enrichEventsWithSeriesList ( page . events ) ;
245+ return page ;
236246 } catch ( error : any ) {
237247 throw kalshiErrorMapper . mapError ( error ) ;
238248 }
@@ -366,14 +376,17 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
366376 return data . orders || [ ] ;
367377 }
368378
369- async fetchRawSeriesMap ( ) : Promise < Map < string , string [ ] > > {
379+ async fetchRawSeriesMap ( ) : Promise < Map < string , KalshiSeriesInfo > > {
370380 try {
371381 const data = await this . ctx . callApi ( 'GetSeriesList' ) ;
372382 const seriesList = data . series || [ ] ;
373- const map = new Map < string , string [ ] > ( ) ;
383+ const map = new Map < string , KalshiSeriesInfo > ( ) ;
374384 for ( const series of seriesList ) {
375- if ( series . tags && series . tags . length > 0 ) {
376- map . set ( series . ticker , series . tags ) ;
385+ if ( series . ticker ) {
386+ map . set ( series . ticker , {
387+ title : typeof series . title === 'string' ? series . title : undefined ,
388+ tags : Array . isArray ( series . tags ) ? series . tags : undefined ,
389+ } ) ;
377390 }
378391 }
379392 return map ;
@@ -394,19 +407,23 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
394407 const event = data . event ;
395408 if ( ! event ) return [ ] ;
396409
397- // Enrich with series tags
410+ // Enrich with series metadata. The series title helps the normalizer
411+ // avoid polluted event titles on multi-market futures.
398412 if ( event . series_ticker ) {
399413 try {
400414 const seriesData = await this . ctx . callApi ( 'GetSeries' , {
401415 series_ticker : event . series_ticker ,
402416 } ) ;
403417 const series = seriesData . series ;
418+ if ( typeof series ?. title === 'string' && series . title . trim ( ) ) {
419+ event . series_title = series . title . trim ( ) ;
420+ }
404421 if ( series ?. tags ?. length > 0 && ( ! event . tags || event . tags . length === 0 ) ) {
405422 event . tags = series . tags ;
406423 }
407424 } catch ( err : unknown ) {
408- // Non-critical — tags are enrichment only.
409- logger . warn ( 'kalshi: series tag fetch failed' , {
425+ // Non-critical — series metadata is enrichment only.
426+ logger . warn ( 'kalshi: series metadata fetch failed' , {
410427 series_ticker : event . series_ticker ,
411428 error : err instanceof Error ? err . message : String ( err ) ,
412429 } ) ;
@@ -438,14 +455,7 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
438455 this . fetchRawSeriesMap ( ) ,
439456 ] ) ;
440457
441- // Enrich events with series tags
442- for ( const event of allEvents ) {
443- if ( event . series_ticker && fetchedSeriesMap . has ( event . series_ticker ) ) {
444- if ( ! event . tags || event . tags . length === 0 ) {
445- event . tags = fetchedSeriesMap . get ( event . series_ticker ) ;
446- }
447- }
448- }
458+ this . enrichEventsWithSeriesMap ( allEvents , fetchedSeriesMap ) ;
449459
450460 if ( fetchLimit >= 1000 && useCache ) {
451461 this . cachedEvents = allEvents ;
@@ -456,6 +466,40 @@ export class KalshiFetcher implements IExchangeFetcher<KalshiRawEvent, KalshiRaw
456466 return allEvents ;
457467 }
458468
469+ private async enrichEventsWithSeriesList ( events : KalshiRawEvent [ ] ) : Promise < KalshiRawEvent [ ] > {
470+ if ( events . length === 0 ) return events ;
471+
472+ try {
473+ const seriesMap = await this . fetchRawSeriesMap ( ) ;
474+ this . enrichEventsWithSeriesMap ( events , seriesMap ) ;
475+ } catch ( err : unknown ) {
476+ // Non-critical — callers can still normalize the venue-native title.
477+ logger . warn ( 'kalshi: series list enrichment failed' , {
478+ error : err instanceof Error ? err . message : String ( err ) ,
479+ } ) ;
480+ }
481+
482+ return events ;
483+ }
484+
485+ private enrichEventsWithSeriesMap (
486+ events : KalshiRawEvent [ ] ,
487+ seriesMap : Map < string , KalshiSeriesInfo > ,
488+ ) : void {
489+ for ( const event of events ) {
490+ if ( ! event . series_ticker ) continue ;
491+ const seriesInfo = seriesMap . get ( event . series_ticker ) ;
492+ if ( ! seriesInfo ) continue ;
493+
494+ if ( seriesInfo . title ) {
495+ event . series_title = seriesInfo . title ;
496+ }
497+ if ( seriesInfo . tags ?. length && ( ! event . tags || event . tags . length === 0 ) ) {
498+ event . tags = seriesInfo . tags ;
499+ }
500+ }
501+ }
502+
459503 private async fetchActiveEvents ( targetMarketCount ?: number , status : string = 'open' ) : Promise < KalshiRawEvent [ ] > {
460504 let allEvents : KalshiRawEvent [ ] = [ ] ;
461505 let totalMarketCount = 0 ;
0 commit comments