Skip to content

Commit baced3c

Browse files
committed
route: add Polymarket
1 parent d6f8ca5 commit baced3c

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

lib/routes/polymarket/markets.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import type { Route } from '@/types';
2+
import ofetch from '@/utils/ofetch';
3+
import { parseDate } from '@/utils/parse-date';
4+
import { load } from 'cheerio';
5+
6+
export const route: Route = {
7+
path: '/:category?',
8+
categories: ['finance'],
9+
example: '/polymarket/trending',
10+
parameters: {
11+
category: {
12+
description: 'Category slug, e.g. trending, breaking, politics, geopolitics, crypto, finance, iran, economy, tech, sports, culture',
13+
default: 'trending',
14+
},
15+
},
16+
features: {
17+
requireConfig: false,
18+
requirePuppeteer: false,
19+
antiCrawler: false,
20+
supportBT: false,
21+
supportPodcast: false,
22+
supportScihub: false,
23+
},
24+
radar: [
25+
{
26+
source: ['polymarket.com', 'polymarket.com/:category'],
27+
target: '/:category',
28+
},
29+
],
30+
name: 'Markets',
31+
url: 'polymarket.com',
32+
maintainers: ['heki'],
33+
handler,
34+
};
35+
36+
// Helper function to find query with events data
37+
function findEventsQuery(queries, category) {
38+
for (const query of queries) {
39+
const queryKey = query?.queryKey || [];
40+
const data = query?.state?.data;
41+
42+
// Check if this query has pages with events
43+
if (data?.pages?.[0]?.events?.length > 0) {
44+
// For category pages, check if queryKey matches the category
45+
if (category !== 'trending') {
46+
const keyStr = JSON.stringify(queryKey);
47+
if (keyStr.includes(category) || keyStr.includes('markets')) {
48+
return data.pages;
49+
}
50+
} else {
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
101+
? market.outcomePrices.map((price, i) => `Option ${i + 1}: ${(Number(price) * 100).toFixed(1)}%`).join(' | ')
102+
: '';
103+
104+
return {
105+
title: market.question,
106+
description: `
107+
<p><strong>Odds:</strong> ${outcomes}</p>
108+
<p><strong>24h Change:</strong> ${market.oneDayPriceChange ? (market.oneDayPriceChange * 100).toFixed(1) + '%' : 'N/A'}</p>
109+
${market.image ? `<img src="${market.image}" alt="${market.question}" style="max-width: 100%;">` : ''}
110+
`,
111+
link: `${baseUrl}/event/${market.slug}`,
112+
pubDate: parseDate(market.events?.[0]?.startDate || market.updatedAt),
113+
};
114+
});
115+
} else {
116+
// Trending or category pages: find events array
117+
const pages = findEventsQuery(queries, category);
118+
119+
if (!pages) {
120+
throw new Error('No events found for this category');
121+
}
122+
123+
const events = pages[0]?.events || [];
124+
125+
items = events.map((event) => {
126+
// Build description from markets
127+
const marketsHtml = event.markets
128+
?.slice(0, 3)
129+
.map((market) => {
130+
const outcomes = market.outcomes || [];
131+
const prices = market.outcomePrices || [];
132+
const oddsDisplay = outcomes.map((o, i) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ');
133+
return `<li><strong>${market.question}</strong><br>${oddsDisplay}</li>`;
134+
})
135+
.join('') || '';
136+
137+
return {
138+
title: event.title,
139+
description: `
140+
${event.description ? `<p>${event.description}</p>` : ''}
141+
<p><strong>Volume:</strong> $${Number(event.volume || 0).toLocaleString()}</p>
142+
${event.live ? '<p>🔴 <strong>LIVE</strong></p>' : ''}
143+
${marketsHtml ? `<h4>Markets:</h4><ul>${marketsHtml}</ul>` : ''}
144+
${event.image ? `<img src="${event.image}" alt="${event.title}" style="max-width: 100%;">` : ''}
145+
`,
146+
link: `${baseUrl}/event/${event.slug}`,
147+
pubDate: parseDate(event.startDate || event.createdAt),
148+
category: event.tags?.map((t) => t.label || t) || [],
149+
};
150+
});
151+
}
152+
153+
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
154+
155+
return {
156+
title: `Polymarket - ${categoryName}`,
157+
link: url,
158+
item: items,
159+
};
160+
}

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)