Skip to content

feat(predictions): add Kalshi as prediction market data source#1355

Merged
koala73 merged 10 commits intokoala73:mainfrom
Rau1CS:feature/kalshi-prediction-source
Mar 12, 2026
Merged

feat(predictions): add Kalshi as prediction market data source#1355
koala73 merged 10 commits intokoala73:mainfrom
Rau1CS:feature/kalshi-prediction-source

Conversation

@Rau1CS
Copy link
Copy Markdown
Contributor

@Rau1CS Rau1CS commented Mar 10, 2026

Summary

  • Add Kalshi (CFTC-regulated exchange) as a second prediction market source alongside Polymarket
  • Both sources fetched in parallel with separate Redis caching and graceful degradation
  • Kalshi markets routed to the finance variant with source badge displayed on each market item

Test plan

  • Run npm run typecheck — no new errors
  • Run npm run dev (full variant) — predictions panel loads with Polymarket markets
  • Run npm run dev:finance — predictions panel shows Kalshi + economy Polymarket markets
  • Verify source badge ("Polymarket" / "Kalshi") renders on each prediction item
  • Verify tooltip mentions both sources
  • Test with Kalshi API down — should degrade gracefully to Polymarket-only

Closes #1285

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 10, 2026

@Rau1CS is attempting to deploy a commit to the Elie Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Owner

@koala73 koala73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding Kalshi as a second prediction source, @Rau1CS! Great work on the parallel fetch pattern and graceful degradation. A few items to address:


BLOCKING

  1. list-prediction-markets.ts:190 — Kalshi fetch fires unconditionally, even when not needed
    kalshiFetch starts on every request, including tech-scoped ones where includeKalshi (line 278) will be false. This wastes a network call + Redis read for every tech panel load. Restructure so the category check happens before kicking off the fetch.

  2. list-prediction-markets.ts:214 — Bootstrap source detection is fragile

    (m as unknown as { source?: string }).source === 'kalshi'

    Double cast through unknown is a code smell. Define a BootstrapMarket interface and use it for the bootstrap type instead.

  3. PredictionPanel.ts:48 — Source comparison uses raw string, not the enum value

    const sourceLabel = p.source === 'kalshi' ? 'Kalshi' : 'Polymarket';

    The proto sends MarketSource enum values (MARKET_SOURCE_KALSHI), but the panel compares against 'kalshi'. This will likely always show "Polymarket". Verify which format the client actually receives.


SUGGESTION

  1. seed-prediction-markets.mjs:108 — Kalshi volume filtering threshold is low.
    volume <= 1000 is very permissive compared to Polymarket's minVolume: 5000 in the Gamma API params. Consider raising for signal quality parity.

  2. list-prediction-markets.ts:162-170 vs seed-prediction-markets.mjs:108-118 — Different market selection logic.
    The seed script picks the highest-volume market per event (via reduce), but the RPC handler picks the first active binary market (break after first match). These should use the same selection logic.

  3. seed-prediction-markets.mjs:117 — Falsy zero-price bug

    const yesPrice = +(parseFloat(topMarket.last_price_dollars) * 100).toFixed(1) || 50;

    If last_price_dollars is "0", this evaluates to 0 (falsy), falling back to 50. A zero-price market would incorrectly show 50%. Use ?? 50 instead of || 50.

  4. No CSS for .prediction-source badge. The panel adds <span class="prediction-source"> but no styles are included in the diff. Will render as unstyled inline text.

  5. FINANCE_TAGS duplicated across seed-prediction-markets.mjs, list-prediction-markets.ts (inline check), and src/services/prediction/index.ts. Consider a shared constant.

  6. Kalshi API URL uses api.elections.kalshi.com, which is the elections-specific subdomain. Verify this returns non-election markets too, or use the general trading-api.kalshi.com endpoint.


NITPICK

  1. open_interest is added to the proto but always 0 for Polymarket and never displayed. Consider deferring this proto field if it's not used yet.

  2. Generated files (service_client.ts, service_server.ts) appear committed manually. Confirm these match buf generate output.

Rau1CS and others added 3 commits March 10, 2026 11:55
- Gate Kalshi fetch behind category check to avoid wasted calls on tech-scoped requests
- Replace fragile double-cast bootstrap typing with BootstrapMarket interface
- Fix zero-price falsy bug in seed script using Number.isFinite guard
- Align RPC market selection with seed script (highest-volume via single-pass loop)
- Raise Kalshi volume threshold to 5000 for signal quality parity
- Add missing .prediction-source badge CSS with per-source color variants
@Rau1CS
Copy link
Copy Markdown
Contributor Author

Rau1CS commented Mar 10, 2026

Thanks for the thorough review! All items addressed in e767132 — here's the rundown:

BLOCKING (all fixed)

  1. Kalshi fetch gated behind includeKalshi check before kickoff — tech-scoped requests no longer fire the call
  2. Bootstrap typing replaced with explicit BootstrapMarket / BootstrapData interfaces, double cast removed
  3. Panel source comparison verified correct — protoToMarket normalizes enum to 'kalshi'/'polymarket' before it reaches the panel

SUGGESTIONS (all fixed)
4. Kalshi volume threshold raised to 5000
5. RPC handler now uses same highest-volume reduce logic as seed script (single-pass loop)
6. Zero-price bug fixed with Number.isFinite guard instead of ||/??"0" now correctly yields 0, not 50
7. Added .prediction-source CSS with per-source color variants (blue for Kalshi, purple for Polymarket)

Items 8-11 (shared constants, Kalshi API subdomain, open_interest proto field, generated files) not addressed in this pass — happy to follow up if you'd like.

Copy link
Copy Markdown
Owner

@koala73 koala73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay on this review, @Rau1CS. Nice work adding Kalshi as a second source. The parallel fetch pattern and graceful degradation are well done. A few issues to address:

P1: Live Kalshi path missing curation filters (seed/RPC divergence)

The seed script applies isExcluded() and a volume <= 5000 threshold, but fetchKalshiMarkets() in list-prediction-markets.ts applies neither. Users hitting the live RPC fallback (cache miss or stale bootstrap) will see unfiltered, low-volume, and potentially sports/entertainment markets that the seed path would have excluded.

Files: server/worldmonitor/prediction/v1/list-prediction-markets.ts (around line 184), scripts/seed-prediction-markets.mjs (line 113)

P2: FINANCE_TAGS includes markets but seed never fetches it

allTags in the seed script is [...GEOPOLITICAL_TAGS, ...TECH_TAGS]. The markets tag from FINANCE_TAGS is never fetched from the Gamma API, so finance bootstrap is missing those Polymarket events entirely.

File: scripts/seed-prediction-markets.mjs (line 148)

P2: Kalshi title mapping differs between seed and RPC

Seed uses topMarket.title || event.title (line 134), but the RPC handler uses market.yes_sub_title || market.title (line 150). The same contract can render with a different label depending on which code path served it, which also affects query filtering behavior.

Files: scripts/seed-prediction-markets.mjs (line 134), server/worldmonitor/prediction/v1/list-prediction-markets.ts (line 150)

P2: Finance variant falls back to geopolitical on cache miss

Both src/services/prediction/index.ts (line 64) and list-prediction-markets.ts (line 220) fall back to bootstrap.geopolitical when bootstrap.finance is absent. Older cached payloads without a finance key will silently serve geopolitical data to finance-variant users instead of falling through to the RPC path.

Files: src/services/prediction/index.ts (line 64), server/worldmonitor/prediction/v1/list-prediction-markets.ts (line 220)

Rau1CS and others added 2 commits March 12, 2026 20:03
- Apply isExcluded() filter and volume threshold (5000) to live Kalshi
  RPC path so cache-miss results match seed curation quality
- Include FINANCE_TAGS in seed allTags so 'markets' tag is fetched
- Align Kalshi title mapping (market.title || event.title) between
  seed and RPC handler
- Remove silent geopolitical fallback for finance variant so missing
  finance bootstrap falls through to RPC fetch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
For multi-contract Kalshi events (e.g. papal election candidates),
market.title is the generic event question while yes_sub_title
identifies the specific contract. Use yes_sub_title when present
in both seed and RPC paths so titles are accurate and consistent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Rau1CS
Copy link
Copy Markdown
Contributor Author

Rau1CS commented Mar 12, 2026

Sorry for the delay — all P1/P2 items from the latest review are addressed in the last two commits. Ready for another look when you get a chance!

@koala73
Copy link
Copy Markdown
Owner

koala73 commented Mar 12, 2026

Thanks a lot for the solid work on this, @Rau1CS! The parallel fetch pattern, graceful degradation, and seed/RPC parity fixes from the last two rounds are all looking great. This is almost ready to merge.

Three suggestions before we close it out:

  1. Kalshi API subdomain: Both the seed script and RPC handler use api.elections.kalshi.com, which is the elections-specific endpoint. Verify it returns non-election markets (economy, crypto, etc.) too, or switch to trading-api.kalshi.com. If elections-only data is coming through, the finance variant will have gaps.

  2. FINANCE_TAGS is defined in 3 places: seed-prediction-markets.mjs, list-prediction-markets.ts, and src/services/prediction/index.ts. If any one drifts, seed/RPC/frontend will disagree on what counts as "finance." Consider extracting to a shared constant (can be a follow-up PR if you prefer).

  3. open_interest proto field: Currently always 0 for Polymarket and not displayed anywhere in the UI. If it's not wired up yet, consider deferring the field to keep the proto lean (also fine as a follow-up).

Items 1 is the most important. 2 and 3 can be follow-up PRs if you'd rather not block on them. Let me know!

Switch from api.elections.kalshi.com (elections-only) to
trading-api.kalshi.com so economy, crypto, and other non-election
markets are included in the finance variant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Rau1CS
Copy link
Copy Markdown
Contributor Author

Rau1CS commented Mar 12, 2026

Thanks for the review!

  1. Kalshi API subdomain — Fixed in ce0e16a. Both seed and RPC now use trading-api.kalshi.com instead of the elections-specific subdomain.
  2. FINANCE_TAGS shared constant — Happy to extract in a follow-up PR.
  3. open_interest proto field — Agreed it's dead weight for now, will clean up in a follow-up.

koala73 added 2 commits March 12, 2026 23:49
Merge main's filterAndScore/tagRegions/input-sanitization changes
with PR's Kalshi integration:
- FINANCE_TAGS: union of both branches (interest-rates, recession,
  trade, tariffs, debt-ceiling from main + crypto, business, markets
  from PR)
- Seed: use filterAndScore() for all variants, add source=kalshi
  filter for finance bucket
- RPC: keep parallel Gamma+Kalshi fetch, use sanitized category/query
  from main, add source + openInterest to bootstrap mapping
- Client: keep both source and regions fields on PredictionMarket
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Mar 12, 2026 7:53pm

Request Review

@koala73 koala73 merged commit af7496c into koala73:main Mar 12, 2026
4 checks passed
aurel10 pushed a commit to aurel10/car_brand_veille that referenced this pull request Mar 20, 2026
…73#1355)

* feat(predictions): add Kalshi as prediction market data source

* fix(predictions): address Kalshi integration review feedback

- Gate Kalshi fetch behind category check to avoid wasted calls on tech-scoped requests
- Replace fragile double-cast bootstrap typing with BootstrapMarket interface
- Fix zero-price falsy bug in seed script using Number.isFinite guard
- Align RPC market selection with seed script (highest-volume via single-pass loop)
- Raise Kalshi volume threshold to 5000 for signal quality parity
- Add missing .prediction-source badge CSS with per-source color variants

* fix(predictions): address P1/P2 review items for Kalshi integration

- Apply isExcluded() filter and volume threshold (5000) to live Kalshi
  RPC path so cache-miss results match seed curation quality
- Include FINANCE_TAGS in seed allTags so 'markets' tag is fetched
- Align Kalshi title mapping (market.title || event.title) between
  seed and RPC handler
- Remove silent geopolitical fallback for finance variant so missing
  finance bootstrap falls through to RPC fetch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(predictions): prefer yes_sub_title for Kalshi multi-contract events

For multi-contract Kalshi events (e.g. papal election candidates),
market.title is the generic event question while yes_sub_title
identifies the specific contract. Use yes_sub_title when present
in both seed and RPC paths so titles are accurate and consistent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(predictions): use general Kalshi trading API subdomain

Switch from api.elections.kalshi.com (elections-only) to
trading-api.kalshi.com so economy, crypto, and other non-election
markets are included in the finance variant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(predictions): add Kalshi as prediction market data source

2 participants