Skip to content

Commit 0d4aa86

Browse files
committed
feat: add Polymarket
1 parent d6f8ca5 commit 0d4aa86

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

lib/routes/polymarket/markets.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { load } from 'cheerio';
2+
3+
import type { Route } from '@/types';
4+
import ofetch from '@/utils/ofetch';
5+
import { parseDate } from '@/utils/parse-date';
6+
7+
export const route: Route = {
8+
path: '/:category?',
9+
categories: ['finance'],
10+
example: '/polymarket/trending',
11+
parameters: {
12+
category: {
13+
description: 'Category slug, e.g. trending, breaking, politics, geopolitics, crypto, finance, iran, economy, tech, sports, culture',
14+
default: 'trending',
15+
},
16+
},
17+
features: {
18+
requireConfig: false,
19+
requirePuppeteer: false,
20+
antiCrawler: false,
21+
supportBT: false,
22+
supportPodcast: false,
23+
supportScihub: false,
24+
},
25+
radar: [
26+
{
27+
source: ['polymarket.com', 'polymarket.com/:category'],
28+
target: '/:category',
29+
},
30+
],
31+
name: 'Markets',
32+
url: 'polymarket.com',
33+
maintainers: ['heki'],
34+
handler,
35+
};
36+
37+
// Helper function to find query with events data
38+
function findEventsQuery(queries, category) {
39+
for (const query of queries) {
40+
const queryKey = query?.queryKey || [];
41+
const data = query?.state?.data;
42+
43+
// Check if this query has pages with events
44+
if (data?.pages?.[0]?.events?.length > 0) {
45+
// For category pages, check if queryKey matches the category
46+
if (category === 'trending') {
47+
return data.pages;
48+
}
49+
const keyStr = JSON.stringify(queryKey);
50+
if (keyStr.includes(category) || keyStr.includes('markets')) {
51+
return data.pages;
52+
}
53+
}
54+
}
55+
return null;
56+
}
57+
58+
async function handler(ctx) {
59+
const category = ctx.req.param('category') || 'trending';
60+
const baseUrl = 'https://polymarket.com';
61+
62+
let url: string;
63+
if (category === 'breaking') {
64+
url = `${baseUrl}/breaking`;
65+
} else if (category === 'trending') {
66+
url = baseUrl;
67+
} else {
68+
url = `${baseUrl}/${category}`;
69+
}
70+
71+
const response = await ofetch(url, {
72+
headers: {
73+
Accept: 'text/html',
74+
},
75+
});
76+
77+
const $ = load(response);
78+
const nextDataScript = $('script#__NEXT_DATA__').html();
79+
80+
if (!nextDataScript) {
81+
throw new Error('Failed to find __NEXT_DATA__');
82+
}
83+
84+
const nextData = JSON.parse(nextDataScript);
85+
const queries = nextData.props.pageProps.dehydratedState.queries;
86+
87+
let items: any[];
88+
89+
if (category === 'breaking') {
90+
// Breaking page: find query with markets array
91+
let markets: any[] = [];
92+
for (const query of queries) {
93+
if (query?.state?.data?.markets?.length > 0) {
94+
markets = query.state.data.markets;
95+
break;
96+
}
97+
}
98+
99+
items = markets.map((market) => {
100+
const outcomes = market.outcomePrices ? market.outcomePrices.map((price, i) => `Option ${i + 1}: ${(Number(price) * 100).toFixed(1)}%`).join(' | ') : '';
101+
102+
return {
103+
title: market.question,
104+
description: `
105+
<p><strong>Odds:</strong> ${outcomes}</p>
106+
<p><strong>24h Change:</strong> ${market.oneDayPriceChange ? (market.oneDayPriceChange * 100).toFixed(1) + '%' : 'N/A'}</p>
107+
${market.image ? `<img src="${market.image}" alt="${market.question}" style="max-width: 100%;">` : ''}
108+
`,
109+
link: `${baseUrl}/event/${market.slug}`,
110+
pubDate: parseDate(market.events?.[0]?.startDate || market.updatedAt),
111+
};
112+
});
113+
} else {
114+
// Trending or category pages: find events array
115+
const pages = findEventsQuery(queries, category);
116+
117+
if (!pages) {
118+
throw new Error('No events found for this category');
119+
}
120+
121+
const events = pages[0]?.events || [];
122+
123+
items = events.map((event) => {
124+
// Build description from markets
125+
const marketsHtml =
126+
event.markets
127+
?.slice(0, 3)
128+
.map((market) => {
129+
const outcomes = market.outcomes || [];
130+
const prices = market.outcomePrices || [];
131+
const oddsDisplay = outcomes.map((o, i) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ');
132+
return `<li><strong>${market.question}</strong><br>${oddsDisplay}</li>`;
133+
})
134+
.join('') || '';
135+
136+
return {
137+
title: event.title,
138+
description: `
139+
${event.description ? `<p>${event.description}</p>` : ''}
140+
<p><strong>Volume:</strong> $${Number(event.volume || 0).toLocaleString()}</p>
141+
${event.live ? '<p>🔴 <strong>LIVE</strong></p>' : ''}
142+
${marketsHtml ? `<h4>Markets:</h4><ul>${marketsHtml}</ul>` : ''}
143+
${event.image ? `<img src="${event.image}" alt="${event.title}" style="max-width: 100%;">` : ''}
144+
`,
145+
link: `${baseUrl}/event/${event.slug}`,
146+
pubDate: parseDate(event.startDate || event.createdAt),
147+
category: event.tags?.map((t) => t.label || t) || [],
148+
};
149+
});
150+
}
151+
152+
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
153+
154+
return {
155+
title: `Polymarket - ${categoryName}`,
156+
link: url,
157+
item: items,
158+
};
159+
}

lib/routes/polymarket/namespace.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { Namespace } from '@/types';
2+
3+
export const namespace: Namespace = {
4+
name: 'Polymarket',
5+
url: 'polymarket.com',
6+
description: `Polymarket is a prediction market platform where you can bet on real-world events.`,
7+
lang: 'en',
8+
};

0 commit comments

Comments
 (0)