From 0d4aa86845d1cd9c68e1cac500205564352135f6 Mon Sep 17 00:00:00 2001 From: "Qi He (Heki)" Date: Thu, 19 Mar 2026 13:50:55 +0800 Subject: [PATCH 1/3] feat: add Polymarket --- lib/routes/polymarket/markets.ts | 159 +++++++++++++++++++++++++++++ lib/routes/polymarket/namespace.ts | 8 ++ 2 files changed, 167 insertions(+) create mode 100644 lib/routes/polymarket/markets.ts create mode 100644 lib/routes/polymarket/namespace.ts diff --git a/lib/routes/polymarket/markets.ts b/lib/routes/polymarket/markets.ts new file mode 100644 index 000000000000..93cea09308ba --- /dev/null +++ b/lib/routes/polymarket/markets.ts @@ -0,0 +1,159 @@ +import { load } from 'cheerio'; + +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/:category?', + categories: ['finance'], + example: '/polymarket/trending', + parameters: { + category: { + description: 'Category slug, e.g. trending, breaking, politics, geopolitics, crypto, finance, iran, economy, tech, sports, culture', + default: 'trending', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['polymarket.com', 'polymarket.com/:category'], + target: '/:category', + }, + ], + name: 'Markets', + url: 'polymarket.com', + maintainers: ['heki'], + handler, +}; + +// Helper function to find query with events data +function findEventsQuery(queries, category) { + for (const query of queries) { + const queryKey = query?.queryKey || []; + const data = query?.state?.data; + + // Check if this query has pages with events + if (data?.pages?.[0]?.events?.length > 0) { + // For category pages, check if queryKey matches the category + if (category === 'trending') { + return data.pages; + } + const keyStr = JSON.stringify(queryKey); + if (keyStr.includes(category) || keyStr.includes('markets')) { + return data.pages; + } + } + } + return null; +} + +async function handler(ctx) { + const category = ctx.req.param('category') || 'trending'; + const baseUrl = 'https://polymarket.com'; + + let url: string; + if (category === 'breaking') { + url = `${baseUrl}/breaking`; + } else if (category === 'trending') { + url = baseUrl; + } else { + url = `${baseUrl}/${category}`; + } + + const response = await ofetch(url, { + headers: { + Accept: 'text/html', + }, + }); + + const $ = load(response); + const nextDataScript = $('script#__NEXT_DATA__').html(); + + if (!nextDataScript) { + throw new Error('Failed to find __NEXT_DATA__'); + } + + const nextData = JSON.parse(nextDataScript); + const queries = nextData.props.pageProps.dehydratedState.queries; + + let items: any[]; + + if (category === 'breaking') { + // Breaking page: find query with markets array + let markets: any[] = []; + for (const query of queries) { + if (query?.state?.data?.markets?.length > 0) { + markets = query.state.data.markets; + break; + } + } + + items = markets.map((market) => { + const outcomes = market.outcomePrices ? market.outcomePrices.map((price, i) => `Option ${i + 1}: ${(Number(price) * 100).toFixed(1)}%`).join(' | ') : ''; + + return { + title: market.question, + description: ` +

Odds: ${outcomes}

+

24h Change: ${market.oneDayPriceChange ? (market.oneDayPriceChange * 100).toFixed(1) + '%' : 'N/A'}

+ ${market.image ? `${market.question}` : ''} + `, + link: `${baseUrl}/event/${market.slug}`, + pubDate: parseDate(market.events?.[0]?.startDate || market.updatedAt), + }; + }); + } else { + // Trending or category pages: find events array + const pages = findEventsQuery(queries, category); + + if (!pages) { + throw new Error('No events found for this category'); + } + + const events = pages[0]?.events || []; + + items = events.map((event) => { + // Build description from markets + const marketsHtml = + event.markets + ?.slice(0, 3) + .map((market) => { + const outcomes = market.outcomes || []; + const prices = market.outcomePrices || []; + const oddsDisplay = outcomes.map((o, i) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | '); + return `
  • ${market.question}
    ${oddsDisplay}
  • `; + }) + .join('') || ''; + + return { + title: event.title, + description: ` + ${event.description ? `

    ${event.description}

    ` : ''} +

    Volume: $${Number(event.volume || 0).toLocaleString()}

    + ${event.live ? '

    🔴 LIVE

    ' : ''} + ${marketsHtml ? `

    Markets:

    ` : ''} + ${event.image ? `${event.title}` : ''} + `, + link: `${baseUrl}/event/${event.slug}`, + pubDate: parseDate(event.startDate || event.createdAt), + category: event.tags?.map((t) => t.label || t) || [], + }; + }); + } + + const categoryName = category.charAt(0).toUpperCase() + category.slice(1); + + return { + title: `Polymarket - ${categoryName}`, + link: url, + item: items, + }; +} diff --git a/lib/routes/polymarket/namespace.ts b/lib/routes/polymarket/namespace.ts new file mode 100644 index 000000000000..04453b660580 --- /dev/null +++ b/lib/routes/polymarket/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Polymarket', + url: 'polymarket.com', + description: `Polymarket is a prediction market platform where you can bet on real-world events.`, + lang: 'en', +}; \ No newline at end of file From c8de38e1d975e26c138fa990c6c0b2b3ee92714e Mon Sep 17 00:00:00 2001 From: "Qi He (Heki)" Date: Fri, 20 Mar 2026 11:09:45 +0800 Subject: [PATCH 2/3] feat(route): add Polymarket routes using Gamma API Routes: - /polymarket/events/:tag_slug? - List events by category - /polymarket/event/:slug - Event details with markets - /polymarket/search/:query - Keyword subscription - /polymarket/series/:slug? - Recurring event series - /polymarket/user/:address - User trading activity - /polymarket/positions/:address - User positions - /polymarket/leaderboard/:category?/:timePeriod? - Trader rankings APIs used: - Gamma API (gamma-api.polymarket.com): events, series - Data API (data-api.polymarket.com): activity, positions, leaderboard Co-Authored-By: Claude Opus 4.6 --- lib/routes/polymarket/event.ts | 100 +++++++++++++++++ lib/routes/polymarket/events.ts | 107 ++++++++++++++++++ lib/routes/polymarket/leaderboard.ts | 80 ++++++++++++++ lib/routes/polymarket/markets.ts | 159 --------------------------- lib/routes/polymarket/positions.ts | 103 +++++++++++++++++ lib/routes/polymarket/search.ts | 100 +++++++++++++++++ lib/routes/polymarket/series.ts | 149 +++++++++++++++++++++++++ lib/routes/polymarket/user.ts | 123 +++++++++++++++++++++ 8 files changed, 762 insertions(+), 159 deletions(-) create mode 100644 lib/routes/polymarket/event.ts create mode 100644 lib/routes/polymarket/events.ts create mode 100644 lib/routes/polymarket/leaderboard.ts delete mode 100644 lib/routes/polymarket/markets.ts create mode 100644 lib/routes/polymarket/positions.ts create mode 100644 lib/routes/polymarket/search.ts create mode 100644 lib/routes/polymarket/series.ts create mode 100644 lib/routes/polymarket/user.ts diff --git a/lib/routes/polymarket/event.ts b/lib/routes/polymarket/event.ts new file mode 100644 index 000000000000..13558ef02556 --- /dev/null +++ b/lib/routes/polymarket/event.ts @@ -0,0 +1,100 @@ +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/event/:slug', + categories: ['finance'], + example: '/polymarket/event/presidential-election-winner-2024', + parameters: { + slug: { + description: 'Event slug from the URL (e.g. presidential-election-winner-2024)', + required: true, + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['polymarket.com/event/:slug'], + target: '/event/:slug', + }, + ], + name: 'Event', + url: 'polymarket.com', + maintainers: ['heki'], + handler, +}; + +const API_BASE = 'https://gamma-api.polymarket.com'; + +interface Event { + id: string; + title: string; + slug: string; + description?: string; + volume?: number; + liquidity?: number; + image?: string; + startDate?: string; + endDate?: string; + live?: boolean; + markets?: Market[]; + tags?: Array<{ label?: string }>; +} + +interface Market { + id: string; + question: string; + slug: string; + outcomes?: string; + outcomePrices?: string; + volume?: string; + image?: string; + oneDayPriceChange?: number; + endDate?: string; + startDate?: string; +} + +async function handler(ctx) { + const slug = ctx.req.param('slug'); + + const event = await ofetch(`${API_BASE}/events/slug/${slug}`); + + if (!event) { + throw new Error('Event not found'); + } + + const items = + event.markets?.map((market) => { + const outcomes = market.outcomes ? JSON.parse(market.outcomes) : []; + const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : []; + const oddsDisplay = prices.length > 0 ? outcomes.map((o: string, i: number) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ') : outcomes.join(' | ') || 'N/A'; + + return { + title: market.question, + description: ` +

    Odds: ${oddsDisplay}

    +

    Volume: $${Number(market.volume || 0).toLocaleString()}

    + ${market.oneDayPriceChange !== undefined && market.oneDayPriceChange !== null ? `

    24h Change: ${(market.oneDayPriceChange * 100).toFixed(1)}%

    ` : ''} + ${market.image ? `${market.question}` : ''} + `, + link: `https://polymarket.com/event/${event.slug}`, + pubDate: parseDate(market.startDate || event.startDate), + category: event.tags?.map((t) => t.label).filter(Boolean), + }; + }) || []; + + return { + title: event.title, + link: `https://polymarket.com/event/${event.slug}`, + item: items, + description: event.description, + }; +} diff --git a/lib/routes/polymarket/events.ts b/lib/routes/polymarket/events.ts new file mode 100644 index 000000000000..edcd7fdef5a6 --- /dev/null +++ b/lib/routes/polymarket/events.ts @@ -0,0 +1,107 @@ +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/events/:tag_slug?', + categories: ['finance'], + example: '/polymarket/events', + parameters: { + tag_slug: { + description: 'Tag slug to filter events, e.g. politics, sports, crypto. Omit for all events.', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['polymarket.com', 'polymarket.com/:tag_slug'], + target: '/events/:tag_slug', + }, + ], + name: 'Events', + url: 'polymarket.com', + maintainers: ['heki'], + handler, +}; + +const API_BASE = 'https://gamma-api.polymarket.com'; + +interface Market { + id: string; + question: string; + outcomes?: string; + outcomePrices?: string; +} + +interface Event { + id: string; + title: string; + slug: string; + description?: string; + volume?: number; + image?: string; + startDate?: string; + markets?: Market[]; + tags?: Array<{ label?: string }>; +} + +async function handler(ctx) { + const tagSlug = ctx.req.param('tag_slug'); + const limit = 30; + + const query: Record = { + active: true, + closed: false, + limit, + order: 'volume', + ascending: false, + }; + + if (tagSlug) { + query.tag_slug = tagSlug; + } + + const data = await ofetch(`${API_BASE}/events`, { query }); + + const items = data.map((event) => { + const marketsHtml = + event.markets + ?.slice(0, 3) + .map((market) => { + const outcomes = market.outcomes ? JSON.parse(market.outcomes) : []; + const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : []; + const oddsDisplay = prices.length > 0 ? outcomes.map((o: string, i: number) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ') : outcomes.join(' | ') || 'N/A'; + return `
  • ${market.question}
    ${oddsDisplay}
  • `; + }) + .join('') || ''; + + return { + title: event.title, + description: ` + ${event.description ? `

    ${event.description}

    ` : ''} +

    Volume: $${Number(event.volume || 0).toLocaleString()}

    + ${marketsHtml ? `

    Markets:

      ${marketsHtml}
    ` : ''} + ${event.image ? `${event.title}` : ''} + `, + link: `https://polymarket.com/event/${event.slug}`, + pubDate: parseDate(event.startDate), + category: event.tags?.map((t) => t.label).filter(Boolean), + }; + }); + + const title = tagSlug ? `Polymarket Events - ${tagSlug}` : 'Polymarket Events'; + const link = tagSlug ? `https://polymarket.com/${tagSlug}` : 'https://polymarket.com'; + + return { + title, + link, + item: items, + }; +} diff --git a/lib/routes/polymarket/leaderboard.ts b/lib/routes/polymarket/leaderboard.ts new file mode 100644 index 000000000000..70428d6e3f1a --- /dev/null +++ b/lib/routes/polymarket/leaderboard.ts @@ -0,0 +1,80 @@ +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/leaderboard/:category?/:timePeriod?', + categories: ['finance'], + example: '/polymarket/leaderboard', + parameters: { + category: { + description: 'Market category: OVERALL, POLITICS, SPORTS, CRYPTO, CULTURE, MENTIONS, WEATHER, ECONOMICS, TECH, FINANCE', + default: 'OVERALL', + }, + timePeriod: { + description: 'Time period: DAY, WEEK, MONTH, ALL', + default: 'DAY', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Leaderboard', + url: 'polymarket.com', + maintainers: ['heki'], + handler, +}; + +const API_BASE = 'https://data-api.polymarket.com'; + +interface LeaderboardEntry { + rank: string; + proxyWallet: string; + userName?: string; + vol?: number; + pnl?: number; + profileImage?: string; + xUsername?: string; + verifiedBadge?: boolean; +} + +async function handler(ctx) { + const category = ctx.req.param('category') || 'OVERALL'; + const timePeriod = ctx.req.param('timePeriod') || 'DAY'; + + const data = await ofetch(`${API_BASE}/v1/leaderboard`, { + query: { + category, + timePeriod, + orderBy: 'PNL', + limit: 50, + }, + }); + + const items = data.map((entry) => ({ + title: `#${entry.rank} ${entry.userName || entry.proxyWallet.slice(0, 8) + '...'}`, + description: ` +

    Rank: #${entry.rank}

    +

    PnL: $${Number(entry.pnl || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    +

    Volume: $${Number(entry.vol || 0).toLocaleString()}

    + ${entry.xUsername ? `

    X: @${entry.xUsername}

    ` : ''} + ${entry.verifiedBadge ? '

    ✅ Verified

    ' : ''} + ${entry.profileImage ? `${entry.userName || 'Trader'}` : ''} + `, + link: `https://polymarket.com/portfolio?address=${entry.proxyWallet}`, + author: entry.userName || entry.proxyWallet, + })); + + const categoryName = category.charAt(0).toUpperCase() + category.slice(1).toLowerCase(); + const periodName = timePeriod.charAt(0).toUpperCase() + timePeriod.slice(1).toLowerCase(); + + return { + title: `Polymarket Leaderboard - ${categoryName} (${periodName})`, + link: 'https://polymarket.com/leaderboard', + item: items, + }; +} diff --git a/lib/routes/polymarket/markets.ts b/lib/routes/polymarket/markets.ts deleted file mode 100644 index 93cea09308ba..000000000000 --- a/lib/routes/polymarket/markets.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { load } from 'cheerio'; - -import type { Route } from '@/types'; -import ofetch from '@/utils/ofetch'; -import { parseDate } from '@/utils/parse-date'; - -export const route: Route = { - path: '/:category?', - categories: ['finance'], - example: '/polymarket/trending', - parameters: { - category: { - description: 'Category slug, e.g. trending, breaking, politics, geopolitics, crypto, finance, iran, economy, tech, sports, culture', - default: 'trending', - }, - }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['polymarket.com', 'polymarket.com/:category'], - target: '/:category', - }, - ], - name: 'Markets', - url: 'polymarket.com', - maintainers: ['heki'], - handler, -}; - -// Helper function to find query with events data -function findEventsQuery(queries, category) { - for (const query of queries) { - const queryKey = query?.queryKey || []; - const data = query?.state?.data; - - // Check if this query has pages with events - if (data?.pages?.[0]?.events?.length > 0) { - // For category pages, check if queryKey matches the category - if (category === 'trending') { - return data.pages; - } - const keyStr = JSON.stringify(queryKey); - if (keyStr.includes(category) || keyStr.includes('markets')) { - return data.pages; - } - } - } - return null; -} - -async function handler(ctx) { - const category = ctx.req.param('category') || 'trending'; - const baseUrl = 'https://polymarket.com'; - - let url: string; - if (category === 'breaking') { - url = `${baseUrl}/breaking`; - } else if (category === 'trending') { - url = baseUrl; - } else { - url = `${baseUrl}/${category}`; - } - - const response = await ofetch(url, { - headers: { - Accept: 'text/html', - }, - }); - - const $ = load(response); - const nextDataScript = $('script#__NEXT_DATA__').html(); - - if (!nextDataScript) { - throw new Error('Failed to find __NEXT_DATA__'); - } - - const nextData = JSON.parse(nextDataScript); - const queries = nextData.props.pageProps.dehydratedState.queries; - - let items: any[]; - - if (category === 'breaking') { - // Breaking page: find query with markets array - let markets: any[] = []; - for (const query of queries) { - if (query?.state?.data?.markets?.length > 0) { - markets = query.state.data.markets; - break; - } - } - - items = markets.map((market) => { - const outcomes = market.outcomePrices ? market.outcomePrices.map((price, i) => `Option ${i + 1}: ${(Number(price) * 100).toFixed(1)}%`).join(' | ') : ''; - - return { - title: market.question, - description: ` -

    Odds: ${outcomes}

    -

    24h Change: ${market.oneDayPriceChange ? (market.oneDayPriceChange * 100).toFixed(1) + '%' : 'N/A'}

    - ${market.image ? `${market.question}` : ''} - `, - link: `${baseUrl}/event/${market.slug}`, - pubDate: parseDate(market.events?.[0]?.startDate || market.updatedAt), - }; - }); - } else { - // Trending or category pages: find events array - const pages = findEventsQuery(queries, category); - - if (!pages) { - throw new Error('No events found for this category'); - } - - const events = pages[0]?.events || []; - - items = events.map((event) => { - // Build description from markets - const marketsHtml = - event.markets - ?.slice(0, 3) - .map((market) => { - const outcomes = market.outcomes || []; - const prices = market.outcomePrices || []; - const oddsDisplay = outcomes.map((o, i) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | '); - return `
  • ${market.question}
    ${oddsDisplay}
  • `; - }) - .join('') || ''; - - return { - title: event.title, - description: ` - ${event.description ? `

    ${event.description}

    ` : ''} -

    Volume: $${Number(event.volume || 0).toLocaleString()}

    - ${event.live ? '

    🔴 LIVE

    ' : ''} - ${marketsHtml ? `

    Markets:

      ${marketsHtml}
    ` : ''} - ${event.image ? `${event.title}` : ''} - `, - link: `${baseUrl}/event/${event.slug}`, - pubDate: parseDate(event.startDate || event.createdAt), - category: event.tags?.map((t) => t.label || t) || [], - }; - }); - } - - const categoryName = category.charAt(0).toUpperCase() + category.slice(1); - - return { - title: `Polymarket - ${categoryName}`, - link: url, - item: items, - }; -} diff --git a/lib/routes/polymarket/positions.ts b/lib/routes/polymarket/positions.ts new file mode 100644 index 000000000000..f061c3eca50d --- /dev/null +++ b/lib/routes/polymarket/positions.ts @@ -0,0 +1,103 @@ +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; + +export const route: Route = { + path: '/positions/:address', + categories: ['finance'], + example: '/polymarket/positions/0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b', + parameters: { + address: { + description: 'Wallet address (0x...)', + required: true, + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'User Positions', + url: 'polymarket.com', + maintainers: ['heki'], + handler, +}; + +const DATA_API = 'https://data-api.polymarket.com'; +const GAMMA_API = 'https://gamma-api.polymarket.com'; + +interface Position { + conditionId: string; + size: number; + avgPrice: number; + currentValue: number; + cashPnl: number; + percentPnl: number; + curPrice: number; + title?: string; + slug?: string; + eventSlug?: string; + outcome?: string; + outcomeIndex?: number; + icon?: string; + endDate?: string; +} + +interface PublicProfile { + name?: string; + pseudonym?: string; +} + +async function handler(ctx) { + const address = ctx.req.param('address'); + + // Fetch profile and positions + let profile: PublicProfile | null = null; + let positions: Position[] = []; + + try { + profile = await ofetch(`${GAMMA_API}/public-profile`, { + query: { address }, + }); + } catch { + // Profile not found, continue without it + } + + try { + positions = await ofetch(`${DATA_API}/positions`, { + query: { + user: address, + limit: 50, + sortBy: 'CURRENT', + sortDirection: 'DESC', + }, + }); + } catch { + // Positions not found, continue with empty array + } + + const displayName = profile?.name || profile?.pseudonym || address.slice(0, 8) + '...' + address.slice(-4); + + const items = positions.map((pos) => ({ + title: pos.title || `Position #${pos.conditionId.slice(0, 8)}`, + description: ` +

    Outcome: ${pos.outcome || `#${pos.outcomeIndex}`}

    +

    Size: ${Number(pos.size).toLocaleString()}

    +

    Avg Price: $${Number(pos.avgPrice).toFixed(4)}

    +

    Current Price: $${Number(pos.curPrice).toFixed(4)}

    +

    Current Value: $${Number(pos.currentValue).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    +

    PnL: ${pos.cashPnl >= 0 ? '+' : ''}$${Number(pos.cashPnl).toFixed(2)} (${pos.percentPnl >= 0 ? '+' : ''}${Number(pos.percentPnl).toFixed(1)}%)

    + ${pos.icon ? `${pos.title || 'Position'}` : ''} + `, + link: pos.eventSlug ? `https://polymarket.com/event/${pos.eventSlug}` : pos.slug ? `https://polymarket.com/event/${pos.slug}` : 'https://polymarket.com', + author: displayName, + })); + + return { + title: `Polymarket Positions - ${displayName}`, + link: `https://polymarket.com/portfolio?address=${address}`, + item: items, + }; +} diff --git a/lib/routes/polymarket/search.ts b/lib/routes/polymarket/search.ts new file mode 100644 index 000000000000..c99083d49f66 --- /dev/null +++ b/lib/routes/polymarket/search.ts @@ -0,0 +1,100 @@ +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/search/:query', + categories: ['finance'], + example: '/polymarket/search/trump', + parameters: { + query: { + description: 'Search query', + required: true, + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'Search', + url: 'polymarket.com', + maintainers: ['heki'], + handler, +}; + +const API_BASE = 'https://gamma-api.polymarket.com'; + +interface SearchResult { + events?: Event[]; + tags?: Array<{ id: string; label: string; slug: string; event_count?: number }>; +} + +interface Event { + id: string; + title: string; + slug: string; + description?: string; + volume?: number; + image?: string; + startDate?: string; + markets?: Market[]; + tags?: Array<{ label?: string }>; +} + +interface Market { + id: string; + question: string; + outcomes?: string; + outcomePrices?: string; +} + +async function handler(ctx) { + const query = ctx.req.param('query'); + + const data = await ofetch(`${API_BASE}/public-search`, { + query: { + q: query, + limit_per_type: 30, + }, + }); + + const items: any[] = []; + + if (data.events?.length) { + for (const event of data.events) { + const marketsHtml = + event.markets + ?.slice(0, 3) + .map((market) => { + const outcomes = market.outcomes ? JSON.parse(market.outcomes) : []; + const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : []; + const oddsDisplay = prices.length > 0 ? outcomes.map((o: string, i: number) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ') : outcomes.join(' | ') || 'N/A'; + return `
  • ${market.question}
    ${oddsDisplay}
  • `; + }) + .join('') || ''; + + items.push({ + title: event.title, + description: ` + ${event.description ? `

    ${event.description}

    ` : ''} +

    Volume: $${Number(event.volume || 0).toLocaleString()}

    + ${marketsHtml ? `

    Markets:

      ${marketsHtml}
    ` : ''} + ${event.image ? `${event.title}` : ''} + `, + link: `https://polymarket.com/event/${event.slug}`, + pubDate: parseDate(event.startDate), + category: event.tags?.map((t) => t.label).filter(Boolean), + }); + } + } + + return { + title: `Polymarket Search - ${query}`, + link: `https://polymarket.com/search?q=${encodeURIComponent(query)}`, + item: items, + }; +} diff --git a/lib/routes/polymarket/series.ts b/lib/routes/polymarket/series.ts new file mode 100644 index 000000000000..912195f4386c --- /dev/null +++ b/lib/routes/polymarket/series.ts @@ -0,0 +1,149 @@ +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/series/:slug?', + categories: ['finance'], + example: '/polymarket/series', + parameters: { + slug: { + description: 'Series slug, e.g. nfl, nba, mlb. If omitted, lists all series.', + default: 'all', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['polymarket.com/series/:slug'], + target: '/series/:slug', + }, + ], + name: 'Series', + url: 'polymarket.com', + maintainers: ['heki'], + handler, +}; + +const API_BASE = 'https://gamma-api.polymarket.com'; + +interface Series { + id: string; + title: string; + slug: string; + description?: string; + image?: string; + volume?: number; + liquidity?: number; + startDate?: string; + createdAt?: string; + updatedAt?: string; + events?: Event[]; +} + +interface Event { + id: string; + title: string; + slug: string; + description?: string; + volume?: number; + image?: string; + startDate?: string; + markets?: Market[]; + tags?: Array<{ label?: string }>; +} + +interface Market { + id: string; + question: string; + outcomes?: string; + outcomePrices?: string; +} + +async function handler(ctx) { + const slug = ctx.req.param('slug'); + const limit = 20; + + if (slug) { + // Get specific series by slug + const data = await ofetch(`${API_BASE}/series`, { + query: { + slug, + limit: 1, + }, + }); + + if (!data.length) { + throw new Error('Series not found'); + } + + const series = data[0]; + const events = series.events || []; + + const items = events.map((event) => { + const marketsHtml = + event.markets + ?.slice(0, 3) + .map((market) => { + const outcomes = market.outcomes ? JSON.parse(market.outcomes) : []; + const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : []; + const oddsDisplay = prices.length > 0 ? outcomes.map((o: string, i: number) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ') : outcomes.join(' | ') || 'N/A'; + return `
  • ${market.question}
    ${oddsDisplay}
  • `; + }) + .join('') || ''; + + return { + title: event.title, + description: ` + ${event.description ? `

    ${event.description}

    ` : ''} +

    Volume: $${Number(event.volume || 0).toLocaleString()}

    + ${marketsHtml ? `

    Markets:

      ${marketsHtml}
    ` : ''} + ${event.image ? `${event.title}` : ''} + `, + link: `https://polymarket.com/event/${event.slug}`, + pubDate: parseDate(event.startDate), + category: event.tags?.map((t) => t.label).filter(Boolean), + }; + }); + + return { + title: `Polymarket Series - ${series.title}`, + link: `https://polymarket.com/series/${series.slug}`, + item: items, + }; + } else { + // List all series + const data = await ofetch(`${API_BASE}/series`, { + query: { + limit, + order: 'volume', + ascending: false, + }, + }); + + const items = data.map((series) => ({ + title: series.title, + description: ` + ${series.description ? `

    ${series.description}

    ` : ''} +

    Volume: $${Number(series.volume || 0).toLocaleString()}

    +

    Liquidity: $${Number(series.liquidity || 0).toLocaleString()}

    + ${series.image ? `${series.title}` : ''} + `, + link: `https://polymarket.com/series/${series.slug}`, + pubDate: parseDate(series.createdAt), + })); + + return { + title: 'Polymarket - Series', + link: 'https://polymarket.com', + item: items, + }; + } +} diff --git a/lib/routes/polymarket/user.ts b/lib/routes/polymarket/user.ts new file mode 100644 index 000000000000..062798fd99a0 --- /dev/null +++ b/lib/routes/polymarket/user.ts @@ -0,0 +1,123 @@ +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/user/:address', + categories: ['finance'], + example: '/polymarket/user/0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b', + parameters: { + address: { + description: 'Wallet address (0x...)', + required: true, + }, + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'User Activity', + url: 'polymarket.com', + maintainers: ['heki'], + handler, +}; + +const GAMMA_API = 'https://gamma-api.polymarket.com'; +const DATA_API = 'https://data-api.polymarket.com'; + +interface Activity { + timestamp: number; + type: 'TRADE' | 'SPLIT' | 'MERGE' | 'REDEEM' | 'REWARD' | 'CONVERSION' | 'MAKER_REBATE'; + size?: number; + usdcSize?: number; + price?: number; + side?: 'BUY' | 'SELL'; + outcomeIndex?: number; + title?: string; + slug?: string; + eventSlug?: string; + outcome?: string; + icon?: string; + transactionHash?: string; + name?: string; + pseudonym?: string; +} + +interface PublicProfile { + name?: string; + pseudonym?: string; + bio?: string; + proxyWallet?: string; + profileImage?: string; +} + +async function handler(ctx) { + const address = ctx.req.param('address'); + + // Fetch profile and activity + let profile: PublicProfile | null = null; + let activity: Activity[] = []; + + try { + profile = await ofetch(`${GAMMA_API}/public-profile`, { + query: { address }, + }); + } catch { + // Profile not found, continue without it + } + + try { + activity = await ofetch(`${DATA_API}/activity`, { + query: { + user: address, + limit: 50, + sortBy: 'TIMESTAMP', + sortDirection: 'DESC', + }, + }); + } catch { + // Activity not found, continue with empty array + } + + const displayName = profile?.name || profile?.pseudonym || activity[0]?.name || activity[0]?.pseudonym || address.slice(0, 8) + '...' + address.slice(-4); + + const items = activity.map((act) => { + const typeEmoji: Record = { + TRADE: '💱', + SPLIT: '✂️', + MERGE: '🔗', + REDEEM: '💰', + REWARD: '🎁', + CONVERSION: '🔄', + MAKER_REBATE: '💵', + }; + + const typeLabel = `${typeEmoji[act.type] || '📝'} ${act.type}`; + + return { + title: act.title ? `${act.title} - ${act.outcome || `Outcome ${act.outcomeIndex}`}` : typeLabel, + description: ` +

    Type: ${typeLabel}

    + ${act.side ? `

    Side: ${act.side}

    ` : ''} + ${act.price === undefined ? '' : `

    Price: $${act.price.toFixed(4)}

    `} + ${act.size === undefined ? '' : `

    Size: ${act.size.toLocaleString()}

    `} + ${act.usdcSize === undefined ? '' : `

    USDC: $${act.usdcSize.toLocaleString()}

    `} + ${act.icon ? `${act.title || 'Market'}` : ''} + `, + link: act.eventSlug ? `https://polymarket.com/event/${act.eventSlug}` : act.slug ? `https://polymarket.com/event/${act.slug}` : 'https://polymarket.com', + pubDate: parseDate(act.timestamp * 1000), + author: displayName, + }; + }); + + return { + title: `Polymarket User - ${displayName}`, + link: `https://polymarket.com/portfolio?address=${address}`, + item: items, + description: profile?.bio || undefined, + }; +} From dc6c049cfdcfd0ca98cc70ffe538b8144252499e Mon Sep 17 00:00:00 2001 From: "Qi He (Heki)" Date: Fri, 20 Mar 2026 11:31:32 +0800 Subject: [PATCH 3/3] feat(route): add Polymarket routes using Gamma API and Data API Routes: - /polymarket/events/:tagSlug? - List events by category - /polymarket/event/:slug - Event details with markets - /polymarket/search/:query - Keyword subscription - /polymarket/series/:slug? - Recurring event series - /polymarket/user/:address - User trading activity - /polymarket/positions/:address - User positions - /polymarket/leaderboard/:category?/:timePeriod? - Trader rankings APIs used: - Gamma API (gamma-api.polymarket.com): /events/pagination, /events/slug, /series, /public-search - Data API (data-api.polymarket.com): /activity, /positions, /v1/leaderboard --- lib/routes/polymarket/event.ts | 74 +++++------------ lib/routes/polymarket/events.ts | 80 +++++-------------- lib/routes/polymarket/leaderboard.ts | 26 ++---- lib/routes/polymarket/namespace.ts | 2 +- lib/routes/polymarket/positions.ts | 39 ++------- lib/routes/polymarket/search.ts | 75 ++++-------------- lib/routes/polymarket/series.ts | 79 ++++--------------- lib/routes/polymarket/types.ts | 114 +++++++++++++++++++++++++++ lib/routes/polymarket/user.ts | 41 ++-------- lib/routes/polymarket/utils.ts | 21 +++++ 10 files changed, 226 insertions(+), 325 deletions(-) create mode 100644 lib/routes/polymarket/types.ts create mode 100644 lib/routes/polymarket/utils.ts diff --git a/lib/routes/polymarket/event.ts b/lib/routes/polymarket/event.ts index 13558ef02556..a107e710c919 100644 --- a/lib/routes/polymarket/event.ts +++ b/lib/routes/polymarket/event.ts @@ -2,15 +2,16 @@ import type { Route } from '@/types'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import type { Event } from './types'; +import { GAMMA_API } from './types'; +import { formatOddsDisplay } from './utils'; + export const route: Route = { path: '/event/:slug', categories: ['finance'], example: '/polymarket/event/presidential-election-winner-2024', parameters: { - slug: { - description: 'Event slug from the URL (e.g. presidential-election-winner-2024)', - required: true, - }, + slug: 'Event slug from the URL (e.g. presidential-election-winner-2024)', }, features: { requireConfig: false, @@ -28,68 +29,31 @@ export const route: Route = { ], name: 'Event', url: 'polymarket.com', - maintainers: ['heki'], + maintainers: ['heqi201255'], handler, }; -const API_BASE = 'https://gamma-api.polymarket.com'; - -interface Event { - id: string; - title: string; - slug: string; - description?: string; - volume?: number; - liquidity?: number; - image?: string; - startDate?: string; - endDate?: string; - live?: boolean; - markets?: Market[]; - tags?: Array<{ label?: string }>; -} - -interface Market { - id: string; - question: string; - slug: string; - outcomes?: string; - outcomePrices?: string; - volume?: string; - image?: string; - oneDayPriceChange?: number; - endDate?: string; - startDate?: string; -} - async function handler(ctx) { const slug = ctx.req.param('slug'); - const event = await ofetch(`${API_BASE}/events/slug/${slug}`); + const event = await ofetch(`${GAMMA_API}/events/slug/${slug}`); if (!event) { throw new Error('Event not found'); } - const items = - event.markets?.map((market) => { - const outcomes = market.outcomes ? JSON.parse(market.outcomes) : []; - const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : []; - const oddsDisplay = prices.length > 0 ? outcomes.map((o: string, i: number) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ') : outcomes.join(' | ') || 'N/A'; - - return { - title: market.question, - description: ` -

    Odds: ${oddsDisplay}

    -

    Volume: $${Number(market.volume || 0).toLocaleString()}

    - ${market.oneDayPriceChange !== undefined && market.oneDayPriceChange !== null ? `

    24h Change: ${(market.oneDayPriceChange * 100).toFixed(1)}%

    ` : ''} - ${market.image ? `${market.question}` : ''} - `, - link: `https://polymarket.com/event/${event.slug}`, - pubDate: parseDate(market.startDate || event.startDate), - category: event.tags?.map((t) => t.label).filter(Boolean), - }; - }) || []; + const items = event.markets.map((market) => ({ + title: market.question, + description: ` +

    Odds: ${formatOddsDisplay(market)}

    +

    Volume: $${Number(market.volume || 0).toLocaleString()}

    + ${market.oneDayPriceChange ? `

    24h Change: ${(market.oneDayPriceChange * 100).toFixed(1)}%

    ` : ''} + ${market.image ? `${market.question}` : ''} + `, + link: `https://polymarket.com/event/${event.slug}`, + pubDate: market.startDate || event.startDate ? parseDate(market.startDate || event.startDate!) : undefined, + category: event.tags?.map((t) => t.label).filter(Boolean) as string[], + })); return { title: event.title, diff --git a/lib/routes/polymarket/events.ts b/lib/routes/polymarket/events.ts index edcd7fdef5a6..df8c726ea065 100644 --- a/lib/routes/polymarket/events.ts +++ b/lib/routes/polymarket/events.ts @@ -2,14 +2,16 @@ import type { Route } from '@/types'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import type { EventsPagination } from './types'; +import { GAMMA_API } from './types'; +import { formatEventDescription } from './utils'; + export const route: Route = { - path: '/events/:tag_slug?', + path: '/events/:tagSlug?', categories: ['finance'], example: '/polymarket/events', parameters: { - tag_slug: { - description: 'Tag slug to filter events, e.g. politics, sports, crypto. Omit for all events.', - }, + tagSlug: 'Tag slug to filter events, e.g. politics, sports, crypto. Omit for all events.', }, features: { requireConfig: false, @@ -21,39 +23,18 @@ export const route: Route = { }, radar: [ { - source: ['polymarket.com', 'polymarket.com/:tag_slug'], - target: '/events/:tag_slug', + source: ['polymarket.com', 'polymarket.com/:tagSlug'], + target: '/events/:tagSlug', }, ], name: 'Events', url: 'polymarket.com', - maintainers: ['heki'], + maintainers: ['heqi201255'], handler, }; -const API_BASE = 'https://gamma-api.polymarket.com'; - -interface Market { - id: string; - question: string; - outcomes?: string; - outcomePrices?: string; -} - -interface Event { - id: string; - title: string; - slug: string; - description?: string; - volume?: number; - image?: string; - startDate?: string; - markets?: Market[]; - tags?: Array<{ label?: string }>; -} - async function handler(ctx) { - const tagSlug = ctx.req.param('tag_slug'); + const tagSlug = ctx.req.param('tagSlug'); const limit = 30; const query: Record = { @@ -68,40 +49,21 @@ async function handler(ctx) { query.tag_slug = tagSlug; } - const data = await ofetch(`${API_BASE}/events`, { query }); - - const items = data.map((event) => { - const marketsHtml = - event.markets - ?.slice(0, 3) - .map((market) => { - const outcomes = market.outcomes ? JSON.parse(market.outcomes) : []; - const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : []; - const oddsDisplay = prices.length > 0 ? outcomes.map((o: string, i: number) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ') : outcomes.join(' | ') || 'N/A'; - return `
  • ${market.question}
    ${oddsDisplay}
  • `; - }) - .join('') || ''; + const response = await ofetch(`${GAMMA_API}/events/pagination`, { query }); - return { - title: event.title, - description: ` - ${event.description ? `

    ${event.description}

    ` : ''} -

    Volume: $${Number(event.volume || 0).toLocaleString()}

    - ${marketsHtml ? `

    Markets:

      ${marketsHtml}
    ` : ''} - ${event.image ? `${event.title}` : ''} - `, - link: `https://polymarket.com/event/${event.slug}`, - pubDate: parseDate(event.startDate), - category: event.tags?.map((t) => t.label).filter(Boolean), - }; - }); + const data = response.data || []; - const title = tagSlug ? `Polymarket Events - ${tagSlug}` : 'Polymarket Events'; - const link = tagSlug ? `https://polymarket.com/${tagSlug}` : 'https://polymarket.com'; + const items = data.map((event) => ({ + title: event.title, + description: formatEventDescription(event), + link: `https://polymarket.com/event/${event.slug}`, + pubDate: event.startDate ? parseDate(event.startDate) : undefined, + category: event.tags?.map((t) => t.label).filter(Boolean) as string[], + })); return { - title, - link, + title: `Polymarket Events${tagSlug ? ` - ${tagSlug}` : ''}`, + link: tagSlug ? `https://polymarket.com/${tagSlug}` : 'https://polymarket.com', item: items, }; } diff --git a/lib/routes/polymarket/leaderboard.ts b/lib/routes/polymarket/leaderboard.ts index 70428d6e3f1a..6bdef90b5e55 100644 --- a/lib/routes/polymarket/leaderboard.ts +++ b/lib/routes/polymarket/leaderboard.ts @@ -1,6 +1,9 @@ import type { Route } from '@/types'; import ofetch from '@/utils/ofetch'; +import type { LeaderboardEntry } from './types'; +import { DATA_API } from './types'; + export const route: Route = { path: '/leaderboard/:category?/:timePeriod?', categories: ['finance'], @@ -25,28 +28,15 @@ export const route: Route = { }, name: 'Leaderboard', url: 'polymarket.com', - maintainers: ['heki'], + maintainers: ['heqi201255'], handler, }; -const API_BASE = 'https://data-api.polymarket.com'; - -interface LeaderboardEntry { - rank: string; - proxyWallet: string; - userName?: string; - vol?: number; - pnl?: number; - profileImage?: string; - xUsername?: string; - verifiedBadge?: boolean; -} - async function handler(ctx) { const category = ctx.req.param('category') || 'OVERALL'; const timePeriod = ctx.req.param('timePeriod') || 'DAY'; - const data = await ofetch(`${API_BASE}/v1/leaderboard`, { + const data = await ofetch(`${DATA_API}/v1/leaderboard`, { query: { category, timePeriod, @@ -56,11 +46,11 @@ async function handler(ctx) { }); const items = data.map((entry) => ({ - title: `#${entry.rank} ${entry.userName || entry.proxyWallet.slice(0, 8) + '...'}`, + title: `#${entry.rank} ${entry.userName || entry.proxyWallet}`, description: `

    Rank: #${entry.rank}

    -

    PnL: $${Number(entry.pnl || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    -

    Volume: $${Number(entry.vol || 0).toLocaleString()}

    +

    PnL: $${Number(entry.pnl).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    +

    Volume: $${Number(entry.vol).toLocaleString()}

    ${entry.xUsername ? `

    X: @${entry.xUsername}

    ` : ''} ${entry.verifiedBadge ? '

    ✅ Verified

    ' : ''} ${entry.profileImage ? `${entry.userName || 'Trader'}` : ''} diff --git a/lib/routes/polymarket/namespace.ts b/lib/routes/polymarket/namespace.ts index 04453b660580..19454d16db35 100644 --- a/lib/routes/polymarket/namespace.ts +++ b/lib/routes/polymarket/namespace.ts @@ -5,4 +5,4 @@ export const namespace: Namespace = { url: 'polymarket.com', description: `Polymarket is a prediction market platform where you can bet on real-world events.`, lang: 'en', -}; \ No newline at end of file +}; diff --git a/lib/routes/polymarket/positions.ts b/lib/routes/polymarket/positions.ts index f061c3eca50d..de9aa1b94aef 100644 --- a/lib/routes/polymarket/positions.ts +++ b/lib/routes/polymarket/positions.ts @@ -1,15 +1,15 @@ import type { Route } from '@/types'; import ofetch from '@/utils/ofetch'; +import type { Position, PublicProfile } from './types'; +import { DATA_API, GAMMA_API } from './types'; + export const route: Route = { path: '/positions/:address', categories: ['finance'], example: '/polymarket/positions/0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b', parameters: { - address: { - description: 'Wallet address (0x...)', - required: true, - }, + address: 'Wallet address (0x...)', }, features: { requireConfig: false, @@ -21,35 +21,10 @@ export const route: Route = { }, name: 'User Positions', url: 'polymarket.com', - maintainers: ['heki'], + maintainers: ['heqi201255'], handler, }; -const DATA_API = 'https://data-api.polymarket.com'; -const GAMMA_API = 'https://gamma-api.polymarket.com'; - -interface Position { - conditionId: string; - size: number; - avgPrice: number; - currentValue: number; - cashPnl: number; - percentPnl: number; - curPrice: number; - title?: string; - slug?: string; - eventSlug?: string; - outcome?: string; - outcomeIndex?: number; - icon?: string; - endDate?: string; -} - -interface PublicProfile { - name?: string; - pseudonym?: string; -} - async function handler(ctx) { const address = ctx.req.param('address'); @@ -78,7 +53,7 @@ async function handler(ctx) { // Positions not found, continue with empty array } - const displayName = profile?.name || profile?.pseudonym || address.slice(0, 8) + '...' + address.slice(-4); + const displayName = profile?.name || profile?.pseudonym || address; const items = positions.map((pos) => ({ title: pos.title || `Position #${pos.conditionId.slice(0, 8)}`, @@ -89,7 +64,7 @@ async function handler(ctx) {

    Current Price: $${Number(pos.curPrice).toFixed(4)}

    Current Value: $${Number(pos.currentValue).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    PnL: ${pos.cashPnl >= 0 ? '+' : ''}$${Number(pos.cashPnl).toFixed(2)} (${pos.percentPnl >= 0 ? '+' : ''}${Number(pos.percentPnl).toFixed(1)}%)

    - ${pos.icon ? `${pos.title || 'Position'}` : ''} + ${pos.title || 'Position'} `, link: pos.eventSlug ? `https://polymarket.com/event/${pos.eventSlug}` : pos.slug ? `https://polymarket.com/event/${pos.slug}` : 'https://polymarket.com', author: displayName, diff --git a/lib/routes/polymarket/search.ts b/lib/routes/polymarket/search.ts index c99083d49f66..add5c719caad 100644 --- a/lib/routes/polymarket/search.ts +++ b/lib/routes/polymarket/search.ts @@ -2,15 +2,16 @@ import type { Route } from '@/types'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import type { SearchResult } from './types'; +import { GAMMA_API } from './types'; +import { formatEventDescription } from './utils'; + export const route: Route = { path: '/search/:query', categories: ['finance'], example: '/polymarket/search/trump', parameters: { - query: { - description: 'Search query', - required: true, - }, + query: 'Search query', }, features: { requireConfig: false, @@ -22,75 +23,27 @@ export const route: Route = { }, name: 'Search', url: 'polymarket.com', - maintainers: ['heki'], + maintainers: ['heqi201255'], handler, }; -const API_BASE = 'https://gamma-api.polymarket.com'; - -interface SearchResult { - events?: Event[]; - tags?: Array<{ id: string; label: string; slug: string; event_count?: number }>; -} - -interface Event { - id: string; - title: string; - slug: string; - description?: string; - volume?: number; - image?: string; - startDate?: string; - markets?: Market[]; - tags?: Array<{ label?: string }>; -} - -interface Market { - id: string; - question: string; - outcomes?: string; - outcomePrices?: string; -} - async function handler(ctx) { const query = ctx.req.param('query'); - const data = await ofetch(`${API_BASE}/public-search`, { + const data = await ofetch(`${GAMMA_API}/public-search`, { query: { q: query, limit_per_type: 30, }, }); - const items: any[] = []; - - if (data.events?.length) { - for (const event of data.events) { - const marketsHtml = - event.markets - ?.slice(0, 3) - .map((market) => { - const outcomes = market.outcomes ? JSON.parse(market.outcomes) : []; - const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : []; - const oddsDisplay = prices.length > 0 ? outcomes.map((o: string, i: number) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ') : outcomes.join(' | ') || 'N/A'; - return `
  • ${market.question}
    ${oddsDisplay}
  • `; - }) - .join('') || ''; - - items.push({ - title: event.title, - description: ` - ${event.description ? `

    ${event.description}

    ` : ''} -

    Volume: $${Number(event.volume || 0).toLocaleString()}

    - ${marketsHtml ? `

    Markets:

      ${marketsHtml}
    ` : ''} - ${event.image ? `${event.title}` : ''} - `, - link: `https://polymarket.com/event/${event.slug}`, - pubDate: parseDate(event.startDate), - category: event.tags?.map((t) => t.label).filter(Boolean), - }); - } - } + const items = (data.events || []).map((event) => ({ + title: event.title, + description: formatEventDescription(event), + link: `https://polymarket.com/event/${event.slug}`, + pubDate: event.startDate ? parseDate(event.startDate) : undefined, + category: event.tags?.map((t) => t.label).filter(Boolean) as string[], + })); return { title: `Polymarket Search - ${query}`, diff --git a/lib/routes/polymarket/series.ts b/lib/routes/polymarket/series.ts index 912195f4386c..f1c77f023c16 100644 --- a/lib/routes/polymarket/series.ts +++ b/lib/routes/polymarket/series.ts @@ -2,6 +2,10 @@ import type { Route } from '@/types'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import type { Event, Series } from './types'; +import { GAMMA_API } from './types'; +import { formatEventDescription } from './utils'; + export const route: Route = { path: '/series/:slug?', categories: ['finance'], @@ -28,52 +32,17 @@ export const route: Route = { ], name: 'Series', url: 'polymarket.com', - maintainers: ['heki'], + maintainers: ['heqi201255'], handler, }; -const API_BASE = 'https://gamma-api.polymarket.com'; - -interface Series { - id: string; - title: string; - slug: string; - description?: string; - image?: string; - volume?: number; - liquidity?: number; - startDate?: string; - createdAt?: string; - updatedAt?: string; - events?: Event[]; -} - -interface Event { - id: string; - title: string; - slug: string; - description?: string; - volume?: number; - image?: string; - startDate?: string; - markets?: Market[]; - tags?: Array<{ label?: string }>; -} - -interface Market { - id: string; - question: string; - outcomes?: string; - outcomePrices?: string; -} - async function handler(ctx) { const slug = ctx.req.param('slug'); const limit = 20; if (slug) { // Get specific series by slug - const data = await ofetch(`${API_BASE}/series`, { + const data = await ofetch(`${GAMMA_API}/series`, { query: { slug, limit: 1, @@ -87,31 +56,13 @@ async function handler(ctx) { const series = data[0]; const events = series.events || []; - const items = events.map((event) => { - const marketsHtml = - event.markets - ?.slice(0, 3) - .map((market) => { - const outcomes = market.outcomes ? JSON.parse(market.outcomes) : []; - const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : []; - const oddsDisplay = prices.length > 0 ? outcomes.map((o: string, i: number) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ') : outcomes.join(' | ') || 'N/A'; - return `
  • ${market.question}
    ${oddsDisplay}
  • `; - }) - .join('') || ''; - - return { - title: event.title, - description: ` - ${event.description ? `

    ${event.description}

    ` : ''} -

    Volume: $${Number(event.volume || 0).toLocaleString()}

    - ${marketsHtml ? `

    Markets:

      ${marketsHtml}
    ` : ''} - ${event.image ? `${event.title}` : ''} - `, - link: `https://polymarket.com/event/${event.slug}`, - pubDate: parseDate(event.startDate), - category: event.tags?.map((t) => t.label).filter(Boolean), - }; - }); + const items = events.map((event: Event) => ({ + title: event.title, + description: formatEventDescription(event), + link: `https://polymarket.com/event/${event.slug}`, + pubDate: event.startDate ? parseDate(event.startDate) : undefined, + category: event.tags?.map((t) => t.label).filter(Boolean) as string[], + })); return { title: `Polymarket Series - ${series.title}`, @@ -120,7 +71,7 @@ async function handler(ctx) { }; } else { // List all series - const data = await ofetch(`${API_BASE}/series`, { + const data = await ofetch(`${GAMMA_API}/series`, { query: { limit, order: 'volume', @@ -137,7 +88,7 @@ async function handler(ctx) { ${series.image ? `${series.title}` : ''} `, link: `https://polymarket.com/series/${series.slug}`, - pubDate: parseDate(series.createdAt), + pubDate: series.createdAt ? parseDate(series.createdAt) : undefined, })); return { diff --git a/lib/routes/polymarket/types.ts b/lib/routes/polymarket/types.ts new file mode 100644 index 000000000000..fac5dff2131f --- /dev/null +++ b/lib/routes/polymarket/types.ts @@ -0,0 +1,114 @@ +// API Constants +export const GAMMA_API = 'https://gamma-api.polymarket.com'; +export const DATA_API = 'https://data-api.polymarket.com'; + +// Common Interfaces + +export interface Market { + id: string; + question: string; + slug?: string; + outcomes?: string; + outcomePrices?: string; + volume?: string; + image?: string; + oneDayPriceChange?: number; + endDate?: string; + startDate?: string; +} + +export interface Event { + id: string; + title: string; + slug: string; + description?: string; + volume?: number; + image?: string; + startDate?: string; + endDate?: string; + liquidity?: number; + live?: boolean; + markets?: Market[]; + tags?: Array<{ label?: string }>; +} + +export interface Series { + id: string; + title: string; + slug: string; + description?: string; + image?: string; + volume?: number; + liquidity?: number; + startDate?: string; + createdAt?: string; + updatedAt?: string; + events?: Event[]; +} + +export interface PublicProfile { + name?: string; + pseudonym?: string; + bio?: string; + proxyWallet?: string; + profileImage?: string; +} + +export interface Position { + conditionId: string; + size: number; + avgPrice: number; + currentValue: number; + cashPnl: number; + percentPnl: number; + curPrice: number; + title?: string; + slug?: string; + eventSlug?: string; + outcome?: string; + outcomeIndex?: number; + icon: string; + endDate?: string; +} + +export interface Activity { + timestamp: number; + type: 'TRADE' | 'SPLIT' | 'MERGE' | 'REDEEM' | 'REWARD' | 'CONVERSION' | 'MAKER_REBATE'; + size?: number; + usdcSize?: number; + price?: number; + side?: 'BUY' | 'SELL'; + outcomeIndex?: number; + title?: string; + slug?: string; + eventSlug?: string; + outcome?: string; + icon?: string; + transactionHash?: string; + name?: string; + pseudonym?: string; +} + +export interface LeaderboardEntry { + rank: string; + proxyWallet: string; + userName?: string; + vol: number; + pnl: number; + profileImage?: string; + xUsername?: string; + verifiedBadge?: boolean; +} + +export interface EventsPagination { + data: Event[]; + pagination?: { + hasMore: boolean; + totalResults: number; + }; +} + +export interface SearchResult { + events?: Event[]; + tags?: Array<{ id: string; label: string; slug: string; event_count?: number }>; +} diff --git a/lib/routes/polymarket/user.ts b/lib/routes/polymarket/user.ts index 062798fd99a0..c285c2961d03 100644 --- a/lib/routes/polymarket/user.ts +++ b/lib/routes/polymarket/user.ts @@ -2,15 +2,15 @@ import type { Route } from '@/types'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; +import type { Activity, PublicProfile } from './types'; +import { DATA_API, GAMMA_API } from './types'; + export const route: Route = { path: '/user/:address', categories: ['finance'], example: '/polymarket/user/0x7c3db723f1d4d8cb9c550095203b686cb11e5c6b', parameters: { - address: { - description: 'Wallet address (0x...)', - required: true, - }, + address: 'Wallet address (0x...)', }, features: { requireConfig: false, @@ -22,39 +22,10 @@ export const route: Route = { }, name: 'User Activity', url: 'polymarket.com', - maintainers: ['heki'], + maintainers: ['heqi201255'], handler, }; -const GAMMA_API = 'https://gamma-api.polymarket.com'; -const DATA_API = 'https://data-api.polymarket.com'; - -interface Activity { - timestamp: number; - type: 'TRADE' | 'SPLIT' | 'MERGE' | 'REDEEM' | 'REWARD' | 'CONVERSION' | 'MAKER_REBATE'; - size?: number; - usdcSize?: number; - price?: number; - side?: 'BUY' | 'SELL'; - outcomeIndex?: number; - title?: string; - slug?: string; - eventSlug?: string; - outcome?: string; - icon?: string; - transactionHash?: string; - name?: string; - pseudonym?: string; -} - -interface PublicProfile { - name?: string; - pseudonym?: string; - bio?: string; - proxyWallet?: string; - profileImage?: string; -} - async function handler(ctx) { const address = ctx.req.param('address'); @@ -83,7 +54,7 @@ async function handler(ctx) { // Activity not found, continue with empty array } - const displayName = profile?.name || profile?.pseudonym || activity[0]?.name || activity[0]?.pseudonym || address.slice(0, 8) + '...' + address.slice(-4); + const displayName = profile?.name || profile?.pseudonym || address; const items = activity.map((act) => { const typeEmoji: Record = { diff --git a/lib/routes/polymarket/utils.ts b/lib/routes/polymarket/utils.ts new file mode 100644 index 000000000000..4af4b5a8835a --- /dev/null +++ b/lib/routes/polymarket/utils.ts @@ -0,0 +1,21 @@ +import type { Event, Market } from './types'; + +export function formatOddsDisplay(market: Market): string { + const outcomes = market.outcomes ? JSON.parse(market.outcomes) : []; + const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : []; + return prices.length > 0 ? outcomes.map((o: string, i: number) => `${o}: ${(Number(prices[i]) * 100).toFixed(1)}%`).join(' | ') : outcomes.join(' | ') || 'N/A'; +} + +export function formatEventDescription(event: Event): string { + const marketsHtml = (event.markets || []) + .slice(0, 3) + .map((market) => `
  • ${market.question}
    ${formatOddsDisplay(market)}
  • `) + .join(''); + + return ` + ${event.description ? `

    ${event.description}

    ` : ''} +

    Volume: $${Number(event.volume || 0).toLocaleString()}

    + ${marketsHtml ? `

    Markets:

      ${marketsHtml}
    ` : ''} + ${event.image ? `${event.title}` : ''} + `; +}