Skip to content

Commit e789cb6

Browse files
committed
Add Slipstream Connect prediction markets data layer
Typed cleaners and fetch client for the Slipstream Connect API (cross-venue prediction markets over Polymarket, Hyperliquid, and Kalshi), plus a bundled sample dataset used when no API key is configured. The optional key lives at ENV.PLUGIN_API_KEYS.slipstream.
1 parent 01b8cb0 commit e789cb6

5 files changed

Lines changed: 452 additions & 1 deletion

File tree

‎src/envConfig.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ export const asEnvConfig = asObject({
167167
apiKey: asString,
168168
baseUrl: asString
169169
})
170+
),
171+
slipstream: asOptional(
172+
asObject({
173+
apiKey: asString,
174+
baseUrl: asOptional(asString, 'https://api.papi.market')
175+
})
170176
)
171177
}).withRest,
172178
() => ({
@@ -180,7 +186,8 @@ export const asEnvConfig = asObject({
180186
revolut: undefined,
181187
simplex: undefined,
182188
ionia: undefined,
183-
phaze: undefined
189+
phaze: undefined,
190+
slipstream: undefined
184191
})
185192
),
186193
RAMP_PLUGIN_INITS: asOptional(

‎src/locales/strings/enUS.json‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,24 @@
15001500
"coin_rank_see_more": "See More",
15011501
"coin_rank_currency_rates_warning_title": "Currency Rates",
15021502
"coin_rank_currency_rates_warning_message_2s": "The rates shown are quoted in %1$s as a fallback. Market data for your default currency setting (%2$s) is not supported.",
1503+
"title_prediction_markets": "Prediction Markets",
1504+
"prediction_markets_category_sports": "Sports",
1505+
"prediction_markets_category_crypto": "Crypto",
1506+
"prediction_markets_category_macro": "Macro",
1507+
"prediction_markets_category_politics": "Politics",
1508+
"prediction_markets_sample_data": "Showing sample markets. Add a Slipstream Connect API key to load live data.",
1509+
"prediction_markets_error": "Unable to load prediction markets. Please try again later.",
1510+
"prediction_markets_empty": "No matched markets in this category right now.",
1511+
"prediction_markets_venue_prices": "Venue Prices",
1512+
"prediction_markets_best_price": "Best Price",
1513+
"prediction_markets_bid": "Bid",
1514+
"prediction_markets_ask": "Ask",
1515+
"prediction_markets_order_book": "Order Book",
1516+
"prediction_markets_bids": "Bids",
1517+
"prediction_markets_asks": "Asks",
1518+
"prediction_markets_market_details": "Market Details",
1519+
"prediction_markets_volume_24h_1s": "24h volume: %1$s",
1520+
"prediction_markets_resolves_1s": "Resolves: %1$s",
15031521
"form_field_mailing_address_title": "Mailing Address",
15041522
"form_field_personal_information_title": "Personal Information",
15051523
"form_field_title_account_owner": "Account Owner",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { asJSON } from 'cleaners'
2+
3+
import { ENV } from '../../env'
4+
import { debugLog } from '../../util/logger'
5+
import { predictionMarketSampleData } from './slipstreamSampleData'
6+
import {
7+
asPredictionMarkets,
8+
type PredictionMarket,
9+
type PredictionMarketCategory
10+
} from './slipstreamTypes'
11+
12+
const DEFAULT_BASE_URL = 'https://api.papi.market'
13+
14+
export interface PredictionMarketsResult {
15+
markets: PredictionMarket[]
16+
/** True when the bundled sample dataset is shown (no API key configured). */
17+
isSampleData: boolean
18+
}
19+
20+
interface SlipstreamConfig {
21+
apiKey: string
22+
baseUrl: string
23+
}
24+
25+
const getSlipstreamConfig = (): SlipstreamConfig | undefined => {
26+
const config = ENV.PLUGIN_API_KEYS?.slipstream
27+
if (config?.apiKey == null || config.apiKey === '') return undefined
28+
return { apiKey: config.apiKey, baseUrl: config.baseUrl ?? DEFAULT_BASE_URL }
29+
}
30+
31+
/**
32+
* Fetches the matched markets for one category from the Slipstream Connect
33+
* API. Falls back to the bundled sample dataset when no API key is
34+
* configured; live fetch errors are thrown for the caller's error state.
35+
*/
36+
export const fetchPredictionMarkets = async (
37+
category: PredictionMarketCategory
38+
): Promise<PredictionMarketsResult> => {
39+
const config = getSlipstreamConfig()
40+
if (config == null) {
41+
debugLog(
42+
'predictionMarkets',
43+
'No Slipstream Connect API key configured; using sample data'
44+
)
45+
return {
46+
markets: predictionMarketSampleData[category],
47+
isSampleData: true
48+
}
49+
}
50+
51+
const uri = `${config.baseUrl}/connect/markets/${category}`
52+
debugLog('predictionMarkets', 'Fetching', uri)
53+
const response = await fetch(uri, {
54+
headers: { 'X-API-Key': config.apiKey }
55+
})
56+
if (!response.ok) {
57+
throw new Error(
58+
`Slipstream markets/${category} failed: HTTP ${response.status}`
59+
)
60+
}
61+
const text = await response.text()
62+
return { markets: asJSON(asPredictionMarkets)(text), isSampleData: false }
63+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import {
2+
asPredictionMarkets,
3+
type PredictionMarket,
4+
type PredictionMarketCategory
5+
} from './slipstreamTypes'
6+
7+
/**
8+
* Bundled sample markets, shaped exactly like `GET /connect/markets/{category}`
9+
* responses. Shown (with an in-UI notice) when no Slipstream Connect API key
10+
* is configured, so the prediction market scenes stay fully browsable.
11+
*
12+
* The literals run through `asPredictionMarkets` so they are guaranteed to
13+
* match what a live API response would produce.
14+
*/
15+
const sampleData = {
16+
sports: [
17+
{
18+
id: 'hyperliquid:@107|polymarket:0x2a3f9c41',
19+
title: 'Lakers vs Celtics: Lakers win',
20+
category: 'sports',
21+
league: 'nba',
22+
legs: [
23+
{
24+
venue: 'polymarket',
25+
market_id: '0x2a3f9c41',
26+
outcome_id: '71943382',
27+
url: 'https://polymarket.com/event/lakers-vs-celtics',
28+
volume_24h: '184200.5',
29+
resolution_date: '2026-08-21T02:30:00Z'
30+
},
31+
{ venue: 'hyperliquid', market_id: '@107', outcome_id: '#107' }
32+
],
33+
venue_prices: [
34+
{ venue: 'hyperliquid', best_ask: '0.64', best_bid: '0.62' },
35+
{ venue: 'polymarket', best_ask: '0.62', best_bid: '0.60' }
36+
],
37+
book: {
38+
bids: [
39+
{ price: '0.62', size: '1200', venue: 'hyperliquid' },
40+
{ price: '0.60', size: '2400', venue: 'polymarket' },
41+
{ price: '0.59', size: '900', venue: 'polymarket' }
42+
],
43+
asks: [
44+
{ price: '0.62', size: '800', venue: 'polymarket' },
45+
{ price: '0.63', size: '1500', venue: 'polymarket' },
46+
{ price: '0.64', size: '650', venue: 'hyperliquid' }
47+
],
48+
best_bid: '0.62',
49+
best_ask: '0.62'
50+
}
51+
},
52+
{
53+
id: 'kalshi:KXNFLGAME|polymarket:0x8b1d2e77',
54+
title: 'Chiefs win Super Bowl LXI',
55+
category: 'sports',
56+
league: 'nfl',
57+
legs: [
58+
{
59+
venue: 'polymarket',
60+
market_id: '0x8b1d2e77',
61+
outcome_id: '55018221',
62+
url: 'https://polymarket.com/event/super-bowl-lxi',
63+
volume_24h: '96411.0',
64+
resolution_date: '2027-02-08T04:00:00Z'
65+
},
66+
{ venue: 'kalshi', market_id: 'KXNFLGAME', outcome_id: 'KXNFLGAME-YES' }
67+
],
68+
venue_prices: [
69+
{ venue: 'polymarket', best_ask: '0.18', best_bid: '0.17' },
70+
{ venue: 'kalshi', best_ask: '0.19', best_bid: '0.16' }
71+
],
72+
book: {
73+
bids: [
74+
{ price: '0.17', size: '5200', venue: 'polymarket' },
75+
{ price: '0.16', size: '3100', venue: 'kalshi' }
76+
],
77+
asks: [
78+
{ price: '0.18', size: '4400', venue: 'polymarket' },
79+
{ price: '0.19', size: '2800', venue: 'kalshi' }
80+
],
81+
best_bid: '0.17',
82+
best_ask: '0.18'
83+
}
84+
}
85+
],
86+
crypto: [
87+
{
88+
id: 'hyperliquid:@212|polymarket:0x91c4aa08',
89+
title: 'Bitcoin above $150k on Dec 31',
90+
category: 'crypto',
91+
legs: [
92+
{
93+
venue: 'polymarket',
94+
market_id: '0x91c4aa08',
95+
outcome_id: '83726190',
96+
url: 'https://polymarket.com/event/bitcoin-150k-2026',
97+
volume_24h: '412876.2',
98+
resolution_date: '2027-01-01T00:00:00Z'
99+
},
100+
{ venue: 'hyperliquid', market_id: '@212', outcome_id: '#212' }
101+
],
102+
venue_prices: [
103+
{ venue: 'polymarket', best_ask: '0.41', best_bid: '0.40' },
104+
{ venue: 'hyperliquid', best_ask: '0.43', best_bid: '0.39' }
105+
],
106+
book: {
107+
bids: [
108+
{ price: '0.40', size: '8800', venue: 'polymarket' },
109+
{ price: '0.39', size: '4100', venue: 'hyperliquid' }
110+
],
111+
asks: [
112+
{ price: '0.41', size: '6200', venue: 'polymarket' },
113+
{ price: '0.43', size: '2900', venue: 'hyperliquid' }
114+
],
115+
best_bid: '0.40',
116+
best_ask: '0.41'
117+
}
118+
},
119+
{
120+
id: 'polymarket:0x5e77b3c2',
121+
title: 'ETH flips BTC market cap this decade',
122+
category: 'crypto',
123+
legs: [
124+
{
125+
venue: 'polymarket',
126+
market_id: '0x5e77b3c2',
127+
outcome_id: '61054433',
128+
url: 'https://polymarket.com/event/eth-flips-btc',
129+
volume_24h: '15320.8',
130+
resolution_date: '2030-01-01T00:00:00Z'
131+
}
132+
],
133+
venue_prices: [
134+
{ venue: 'polymarket', best_ask: '0.07', best_bid: '0.06' }
135+
],
136+
book: {
137+
bids: [{ price: '0.06', size: '12000', venue: 'polymarket' }],
138+
asks: [{ price: '0.07', size: '9500', venue: 'polymarket' }],
139+
best_bid: '0.06',
140+
best_ask: '0.07'
141+
}
142+
}
143+
],
144+
macro: [
145+
{
146+
id: 'kalshi:KXFEDCUT|polymarket:0x33d90f15',
147+
title: 'Fed cuts rates at the next FOMC meeting',
148+
category: 'macro',
149+
legs: [
150+
{
151+
venue: 'polymarket',
152+
market_id: '0x33d90f15',
153+
outcome_id: '90211675',
154+
url: 'https://polymarket.com/event/fed-cut-next-fomc',
155+
volume_24h: '287554.1',
156+
resolution_date: '2026-09-17T18:00:00Z'
157+
},
158+
{ venue: 'kalshi', market_id: 'KXFEDCUT', outcome_id: 'KXFEDCUT-YES' }
159+
],
160+
venue_prices: [
161+
{ venue: 'polymarket', best_ask: '0.72', best_bid: '0.71' },
162+
{ venue: 'kalshi', best_ask: '0.74', best_bid: '0.70' }
163+
],
164+
book: {
165+
bids: [
166+
{ price: '0.71', size: '10400', venue: 'polymarket' },
167+
{ price: '0.70', size: '5600', venue: 'kalshi' }
168+
],
169+
asks: [
170+
{ price: '0.72', size: '7700', venue: 'polymarket' },
171+
{ price: '0.74', size: '3900', venue: 'kalshi' }
172+
],
173+
best_bid: '0.71',
174+
best_ask: '0.72'
175+
}
176+
}
177+
],
178+
politics: [
179+
{
180+
id: 'kalshi:KXPRES28|polymarket:0xa10c44d9',
181+
title: 'Incumbent party wins 2028 US election',
182+
category: 'politics',
183+
legs: [
184+
{
185+
venue: 'polymarket',
186+
market_id: '0xa10c44d9',
187+
outcome_id: '47700912',
188+
url: 'https://polymarket.com/event/2028-us-election',
189+
volume_24h: '731209.9',
190+
resolution_date: '2028-11-08T05:00:00Z'
191+
},
192+
{ venue: 'kalshi', market_id: 'KXPRES28', outcome_id: 'KXPRES28-YES' }
193+
],
194+
venue_prices: [
195+
{ venue: 'polymarket', best_ask: '0.52', best_bid: '0.51' },
196+
{ venue: 'kalshi', best_ask: '0.53', best_bid: '0.50' }
197+
],
198+
book: {
199+
bids: [
200+
{ price: '0.51', size: '22000', venue: 'polymarket' },
201+
{ price: '0.50', size: '15000', venue: 'kalshi' }
202+
],
203+
asks: [
204+
{ price: '0.52', size: '18000', venue: 'polymarket' },
205+
{ price: '0.53', size: '9000', venue: 'kalshi' }
206+
],
207+
best_bid: '0.51',
208+
best_ask: '0.52'
209+
}
210+
}
211+
]
212+
}
213+
214+
export const predictionMarketSampleData: Record<
215+
PredictionMarketCategory,
216+
PredictionMarket[]
217+
> = {
218+
sports: asPredictionMarkets(sampleData.sports),
219+
crypto: asPredictionMarkets(sampleData.crypto),
220+
macro: asPredictionMarkets(sampleData.macro),
221+
politics: asPredictionMarkets(sampleData.politics)
222+
}

0 commit comments

Comments
 (0)