Created
October 13, 2025 18:01
-
-
Save chand1012/54d98e2f78a6ffa90991d5f586364b6d to your computer and use it in GitHub Desktop.
Single-file typed Polymarket Gamma API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/clients/polymarket-gamma.ts | |
// Purpose: Strongly-typed wrapper around the Polymarket Gamma REST API using fetch. Includes response interfaces and pagination helpers. | |
// ChatGPT Thread: https://chatgpt.com/share/68ed3e92-bb08-8002-bd03-c86e1f8fba96 | |
export type FetchLike = (input: RequestInfo, init?: RequestInit) => Promise<Response>; | |
type Primitive = string | number | boolean; | |
type ParamValue = Primitive | Primitive[]; | |
type Params = Record<string, ParamValue | undefined>; | |
function toQuery(params?: Params): string { | |
if (!params) return ""; | |
const qp = new URLSearchParams(); | |
for (const [k, v] of Object.entries(params)) { | |
if (v === undefined) continue; | |
if (Array.isArray(v)) qp.set(k, v.join(",")); | |
else qp.set(k, String(v)); | |
} | |
const s = qp.toString(); | |
return s ? `?${s}` : ""; | |
} | |
/* ----------------------------- Shared models ----------------------------- */ | |
export interface ImageOptimized { | |
id?: string | null; | |
imageUrlSource?: string | null; | |
imageUrlOptimized?: string | null; | |
imageSizeKbSource?: number | null; | |
imageSizeKbOptimized?: number | null; | |
imageOptimizedComplete?: boolean | null; | |
imageOptimizedLastUpdated?: string | null; // ISO | |
relID?: number | null; | |
field?: string | null; | |
relname?: string | null; | |
} | |
export interface GammaTag { | |
id: string; | |
label?: string | null; | |
slug?: string | null; | |
forceShow?: boolean | null; | |
publishedAt?: string | null; // ISO | |
createdBy?: number | null; | |
updatedBy?: number | null; | |
createdAt?: string | null; // ISO | |
updatedAt?: string | null; // ISO | |
forceHide?: boolean | null; | |
isCarousel?: boolean | null; | |
} | |
export interface Category { | |
id?: string | null; | |
label?: string | null; | |
parentCategory?: string | null; | |
slug?: string | null; | |
publishedAt?: string | null; | |
createdBy?: string | null; | |
updatedBy?: string | null; | |
createdAt?: string | null; // ISO | |
updatedAt?: string | null; // ISO | |
} | |
export interface ChatChannel { | |
id?: string | null; | |
channelId?: string | null; | |
channelName?: string | null; | |
channelImage?: string | null; | |
live?: boolean | null; | |
startTime?: string | null; // ISO | |
endTime?: string | null; // ISO | |
} | |
export interface Series { | |
id?: string | null; | |
ticker?: string | null; | |
slug?: string | null; | |
title?: string | null; | |
subtitle?: string | null; | |
seriesType?: string | null; // e.g., league/season classification | |
recurrence?: string | null; | |
description?: string | null; | |
image?: string | null; | |
icon?: string | null; | |
layout?: string | null; | |
active?: boolean | null; | |
// Sports helpers often appear: | |
spreadsMainLine?: number | null; | |
totalsMainLine?: number | null; | |
carouselMap?: string | null; | |
pendingDeployment?: boolean | null; | |
deploying?: boolean | null; | |
deployingTimestamp?: string | null; // ISO | |
scheduledDeploymentTimestamp?: string | null; // ISO | |
gameStatus?: string | null; | |
} | |
export interface Team { | |
id: number; | |
name?: string | null; | |
league?: string | null; | |
record?: string | null; | |
logo?: string | null; | |
abbreviation?: string | null; | |
alias?: string | null; | |
createdAt?: string | null; // ISO | |
updatedAt?: string | null; // ISO | |
} | |
export interface SportsMeta { | |
sport: string; | |
image?: string | null; | |
resolution?: string | null; | |
ordering?: string | null; // e.g., "home" | "away" | |
tags?: string | null; // CSV of tag ids | |
series?: string | null; // series identifier | |
} | |
/* ------------------------------ Gamma: Event ----------------------------- */ | |
export interface EventLite { | |
id: string; | |
ticker?: string | null; | |
slug: string; | |
title?: string | null; | |
subtitle?: string | null; | |
description?: string | null; | |
resolutionSource?: string | null; | |
startDate?: string | null; // ISO | |
creationDate?: string | null; // ISO | |
endDate?: string | null; // ISO | |
image?: string | null; | |
icon?: string | null; | |
active?: boolean | null; | |
closed?: boolean | null; | |
archived?: boolean | null; | |
new?: boolean | null; | |
featured?: boolean | null; | |
restricted?: boolean | null; | |
liquidity?: number | null; | |
volume?: number | null; | |
openInterest?: number | null; | |
sortBy?: string | null; | |
category?: string | null; | |
subcategory?: string | null; | |
isTemplate?: boolean | null; | |
templateVariables?: string | null; | |
published_at?: string | null; | |
createdBy?: string | null; | |
updatedBy?: string | null; | |
createdAt?: string | null; // ISO | |
updatedAt?: string | null; // ISO | |
commentsEnabled?: boolean | null; | |
competitive?: number | null; | |
volume24hr?: number | null; | |
volume1wk?: number | null; | |
volume1mo?: number | null; | |
volume1yr?: number | null; | |
featuredImage?: string | null; | |
disqusThread?: string | null; | |
parentEvent?: string | null; | |
enableOrderBook?: boolean | null; | |
liquidityAmm?: number | null; | |
liquidityClob?: number | null; | |
negRisk?: boolean | null; | |
negRiskMarketID?: string | null; | |
negRiskFeeBips?: number | null; | |
commentCount?: number | null; | |
imageOptimized?: ImageOptimized; | |
iconOptimized?: ImageOptimized; | |
featuredImageOptimized?: ImageOptimized; | |
subEvents?: string[] | null; | |
// Optional expansions: | |
markets?: MarketLite[]; | |
series?: Series[]; | |
categories?: Category[]; | |
collections?: { | |
id?: string | null; | |
title?: string | null; | |
description?: string | null; | |
headerImage?: string | null; | |
icon?: string | null; | |
slug?: string | null; | |
layout?: string | null; | |
featured?: boolean | null; | |
restricted?: boolean | null; | |
isTemplate?: boolean | null; | |
templateVariables?: string | null; | |
publishedAt?: string | null; | |
createdBy?: string | null; | |
updatedBy?: string | null; | |
createdAt?: string | null; | |
updatedAt?: string | null; | |
commentsEnabled?: boolean | null; | |
imageOptimized?: ImageOptimized; | |
iconOptimized?: ImageOptimized; | |
headerImageOptimized?: ImageOptimized; | |
}[]; | |
tags?: GammaTag[]; | |
chats?: ChatChannel[]; | |
} | |
/* ------------------------------ Gamma: Market ---------------------------- */ | |
export interface MarketLite { | |
id: string; | |
question?: string | null; | |
conditionId?: string | null; | |
slug: string; | |
twitterCardImage?: string | null; | |
resolutionSource?: string | null; | |
endDate?: string | null; // ISO | |
category?: string | null; | |
ammType?: string | null; | |
liquidity?: string | null | number; // docs show string; some fields also as number in practice | |
sponsorName?: string | null; | |
sponsorImage?: string | null; | |
startDate?: string | null; // ISO | |
xAxisValue?: string | null; | |
yAxisValue?: string | null; | |
denominationToken?: string | null; | |
fee?: string | null; | |
image?: string | null; | |
icon?: string | null; | |
lowerBound?: string | null; | |
upperBound?: string | null; | |
description?: string | null; | |
outcomes?: string | null; // comma-separated or array in practice | |
outcomePrices?: string | null; // comma-separated or array in practice | |
volume?: string | number | null; | |
active?: boolean | null; | |
marketType?: string | null; // e.g., categorical/binary/spread | |
formatType?: string | null; | |
lowerBoundDate?: string | null; | |
upperBoundDate?: string | null; | |
closed?: boolean | null; | |
marketMakerAddress?: string | null; | |
createdBy?: number | null; | |
updatedBy?: number | null; | |
createdAt?: string | null; // ISO | |
updatedAt?: string | null; // ISO | |
closedTime?: string | null; | |
wideFormat?: boolean | null; | |
new?: boolean | null; | |
mailchimpTag?: string | null; | |
featured?: boolean | null; | |
archived?: boolean | null; | |
resolvedBy?: string | null; | |
restricted?: boolean | null; | |
marketGroup?: number | null; | |
groupItemTitle?: string | null; | |
groupItemThreshold?: string | null; | |
questionID?: string | null; | |
umaEndDate?: string | null; | |
enableOrderBook?: boolean | null; | |
orderPriceMinTickSize?: number | null; | |
orderMinSize?: number | null; | |
umaResolutionStatus?: string | null; | |
umaResolutionStatuses?: string | null; | |
curationOrder?: number | null; | |
volumeNum?: number | null; | |
liquidityNum?: number | null; | |
endDateIso?: string | null; | |
startDateIso?: string | null; | |
umaEndDateIso?: string | null; | |
hasReviewedDates?: boolean | null; | |
readyForCron?: boolean | null; | |
commentsEnabled?: boolean | null; | |
volume24hr?: number | null; | |
volume1wk?: number | null; | |
volume1mo?: number | null; | |
volume1yr?: number | null; | |
gameStartTime?: string | null; | |
secondsDelay?: number | null; | |
clobTokenIds?: string | null; // CSV | |
disqusThread?: string | null; | |
shortOutcomes?: string | null; // CSV | |
teamAID?: string | null; | |
teamBID?: string | null; | |
umaBond?: string | null; | |
umaReward?: string | null; | |
fpmmLive?: boolean | null; | |
volume24hrAmm?: number | null; | |
volume1wkAmm?: number | null; | |
volume1moAmm?: number | null; | |
volume1yrAmm?: number | null; | |
volume24hrClob?: number | null; | |
volume1wkClob?: number | null; | |
volume1moClob?: number | null; | |
volume1yrClob?: number | null; | |
volumeAmm?: number | null; | |
volumeClob?: number | null; | |
liquidityAmm?: number | null; | |
liquidityClob?: number | null; | |
makerBaseFee?: number | null; | |
takerBaseFee?: number | null; | |
customLiveness?: number | null; | |
acceptingOrders?: boolean | null; | |
notificationsEnabled?: boolean | null; | |
score?: number | null; | |
imageOptimized?: ImageOptimized; | |
iconOptimized?: ImageOptimized; | |
// optional expansions | |
events?: EventLite[]; | |
categories?: Category[]; | |
tags?: GammaTag[]; | |
tweetCount?: number | null; | |
chats?: ChatChannel[]; | |
eventStartTime?: string | null; // ISO | |
rfqEnabled?: boolean | null; | |
// Sports special fields: | |
sportsBook?: string | null; | |
sportsMarketType?: string | null; | |
line?: number | null; | |
} | |
/* ------------------------------ Gamma: Comments -------------------------- */ | |
export interface CommentProfile { | |
name?: string | null; | |
pseudonym?: string | null; | |
displayUsernamePublic?: boolean | null; | |
bio?: string | null; | |
isMod?: boolean | null; | |
isCreator?: boolean | null; | |
proxyWallet?: string | null; | |
baseAddress?: string | null; | |
profileImage?: string | null; | |
profileImageOptimized?: ImageOptimized; | |
positions?: { tokenId: string; positionSize: string }[]; | |
} | |
export interface CommentReactionProfile extends CommentProfile {} | |
export interface CommentReaction { | |
id?: string | null; | |
commentID?: number | null; | |
reactionType?: string | null; | |
icon?: string | null; | |
userAddress?: string | null; | |
createdAt?: string | null; // ISO | |
profile?: CommentReactionProfile; | |
} | |
export interface Comment { | |
id: string; | |
body?: string | null; | |
parentEntityType?: string | null; // "market" | "event" | etc | |
parentEntityID?: number | null; | |
parentCommentID?: string | null; | |
userAddress?: string | null; | |
replyAddress?: string | null; | |
createdAt?: string | null; // ISO | |
updatedAt?: string | null; // ISO | |
profile?: CommentProfile; | |
reactions?: CommentReaction[]; | |
reportCount?: number | null; | |
reactionCount?: number | null; | |
} | |
/* ------------------------------ Gamma: Search ---------------------------- */ | |
export interface PublicSearchPagination { | |
hasMore?: boolean; | |
totalResults?: number; | |
} | |
export interface PublicSearchResponse { | |
events?: EventLite[]; | |
markets?: MarketLite[]; | |
profiles?: Array<{ | |
id?: string; | |
name?: string | null; | |
user?: number | null; | |
referral?: string | null; | |
createdBy?: number | null; | |
updatedBy?: number | null; | |
createdAt?: string | null; | |
updatedAt?: string | null; | |
utmSource?: string | null; | |
utmMedium?: string | null; | |
utmCampaign?: string | null; | |
utmContent?: string | null; | |
utmTerm?: string | null; | |
walletActivated?: boolean | null; | |
pseudonym?: string | null; | |
displayUsernamePublic?: boolean | null; | |
profileImage?: string | null; | |
bio?: string | null; | |
proxyWallet?: string | null; | |
profileImageOptimized?: ImageOptimized; | |
isCloseOnly?: boolean | null; | |
isCertReq?: boolean | null; | |
certReqDate?: string | null; // ISO | |
}>; | |
pagination?: PublicSearchPagination; | |
} | |
/* --------------------------------- Client -------------------------------- */ | |
export interface GammaClientOptions { | |
baseUrl?: string; // default https://gamma-api.polymarket.com | |
fetchImpl?: FetchLike; // default globalThis.fetch | |
defaultHeaders?: HeadersInit; // optional | |
timeoutMs?: number; // default 20000 | |
} | |
export class GammaClient { | |
readonly baseUrl: string; | |
readonly fetchImpl: FetchLike; | |
readonly defaultHeaders: HeadersInit; | |
readonly timeoutMs: number; | |
constructor(opts: GammaClientOptions = {}) { | |
this.baseUrl = opts.baseUrl ?? "https://gamma-api.polymarket.com"; | |
this.fetchImpl = opts.fetchImpl ?? (globalThis.fetch as FetchLike); | |
this.defaultHeaders = opts.defaultHeaders ?? {}; | |
this.timeoutMs = opts.timeoutMs ?? 20_000; | |
} | |
private async get<T>(path: string, params?: Params, headers?: HeadersInit): Promise<T> { | |
const url = `${this.baseUrl}${path}${toQuery(params)}`; | |
const ac = new AbortController(); | |
const id = setTimeout(() => ac.abort(), this.timeoutMs); | |
try { | |
const res = await this.fetchImpl(url, { | |
method: "GET", | |
headers: { ...this.defaultHeaders, ...(headers ?? {}) }, | |
signal: ac.signal, | |
}); | |
if (!res.ok) { | |
const text = await res.text().catch(() => ""); | |
throw new Error(`Gamma GET ${path} ${res.status}: ${text || res.statusText}`); | |
} | |
return (await res.json()) as T; | |
} finally { | |
clearTimeout(id); | |
} | |
} | |
// deno-lint-ignore no-explicit-any | |
async *paginate<T = any>( | |
path: string, | |
params: Params & { limit?: number; offset?: number } = {}, | |
pageSize = 100, | |
maxPages = Infinity, | |
): AsyncGenerator<T[], void, unknown> { | |
let offset = params.offset ?? 0; | |
const limit = params.limit ?? pageSize; | |
let pages = 0; | |
for (;;) { | |
const page = (await this.get<T[]>(path, { ...params, limit, offset })) ?? []; | |
yield page; | |
pages += 1; | |
if (page.length < limit || pages >= maxPages) break; | |
offset += limit; | |
} | |
} | |
/* ------------------------------- Health ------------------------------- */ | |
async health(): Promise<unknown> { | |
try { | |
return await this.get<unknown>("/"); | |
} catch { | |
return await this.get<unknown>("/ok"); | |
} | |
} | |
/* -------------------------------- Sports ------------------------------ */ | |
getSports(): Promise<SportsMeta[]> { | |
return this.get<SportsMeta[]>("/sports"); | |
} | |
listTeams(params?: { | |
limit?: number; offset?: number; order?: string; ascending?: boolean; | |
league?: string[]; name?: string[]; abbreviation?: string[]; | |
}): Promise<Team[]> { | |
return this.get<Team[]>("/teams", params as Params); | |
} | |
/* --------------------------------- Tags ------------------------------- */ | |
listTags(params?: { | |
limit?: number; offset?: number; order?: string; ascending?: boolean; | |
include_template?: boolean; is_carousel?: boolean; | |
}): Promise<GammaTag[]> { | |
return this.get<GammaTag[]>("/tags", params as Params); | |
} | |
getTagById(id: string | number): Promise<GammaTag> { | |
return this.get<GammaTag>(`/tags/${id}`); | |
} | |
getTagBySlug(slug: string): Promise<GammaTag> { | |
return this.get<GammaTag>(`/tags/slug/${encodeURIComponent(slug)}`); | |
} | |
getRelatedTagsById(id: string | number): Promise<GammaTag[]> { | |
return this.get<GammaTag[]>(`/tags/${id}/relationships`); | |
} | |
getRelatedTagsBySlug(slug: string): Promise<GammaTag[]> { | |
return this.get<GammaTag[]>(`/tags/slug/${encodeURIComponent(slug)}/relationships`); | |
} | |
getTagsRelatedToTagId(id: string | number): Promise<GammaTag[]> { | |
return this.get<GammaTag[]>(`/tags/${id}/related`); | |
} | |
getTagsRelatedToTagSlug(slug: string): Promise<GammaTag[]> { | |
return this.get<GammaTag[]>(`/tags/slug/${encodeURIComponent(slug)}/related`); | |
} | |
/* -------------------------------- Events ------------------------------ */ | |
listEvents(params?: Params & { limit?: number; offset?: number }): Promise<EventLite[]> { | |
return this.get<EventLite[]>("/events", params); | |
} | |
getEventById(id: string | number): Promise<EventLite> { | |
return this.get<EventLite>(`/events/${id}`); | |
} | |
getEventBySlug(slug: string): Promise<EventLite> { | |
return this.get<EventLite>(`/events/slug/${encodeURIComponent(slug)}`); | |
} | |
getEventTags(id: string | number): Promise<GammaTag[]> { | |
return this.get<GammaTag[]>(`/events/${id}/tags`); | |
} | |
/* -------------------------------- Markets ----------------------------- */ | |
listMarkets(params?: Params & { limit?: number; offset?: number }): Promise<MarketLite[]> { | |
return this.get<MarketLite[]>("/markets", params); | |
} | |
getMarketById(id: string | number): Promise<MarketLite> { | |
return this.get<MarketLite>(`/markets/${id}`); | |
} | |
getMarketBySlug(slug: string): Promise<MarketLite> { | |
return this.get<MarketLite>(`/markets/slug/${encodeURIComponent(slug)}`); | |
} | |
getMarketTagsById(id: string | number): Promise<GammaTag[]> { | |
return this.get<GammaTag[]>(`/markets/${id}/tags`); | |
} | |
/* -------------------------------- Series ------------------------------ */ | |
getSeriesById(id: string | number): Promise<Series> { | |
return this.get<Series>(`/series/${id}`); | |
} | |
/* ------------------------------- Comments ----------------------------- */ | |
listComments(params?: { | |
limit?: number; offset?: number; order?: string; ascending?: boolean; | |
parent_entity_type?: string; parent_entity_id?: string | number; | |
get_positions?: boolean; holders_only?: boolean; | |
}): Promise<Comment[]> { | |
return this.get<Comment[]>("/comments", params as Params); | |
} | |
getCommentsByCommentId(id: string | number, params?: { get_positions?: boolean }): Promise<Comment[]> { | |
return this.get<Comment[]>(`/comments/${id}`, params as Params); | |
} | |
getCommentsByUserAddress(userAddress: string, params?: { get_positions?: boolean }): Promise<Comment[]> { | |
return this.get<Comment[]>(`/comments/user_address/${encodeURIComponent(userAddress)}`, params as Params); | |
} | |
/* -------------------------------- Search ------------------------------ */ | |
publicSearch(params?: { q?: string; type?: string; limit?: number; offset?: number }): Promise<PublicSearchResponse> { | |
return this.get<PublicSearchResponse>("/public-search", params as Params); | |
} | |
/* ------------------------------ Convenience --------------------------- */ | |
async getAllEvents(params?: Params, pageSize = 100): Promise<EventLite[]> { | |
const out: EventLite[] = []; | |
for await (const page of this.paginate<EventLite>("/events", { ...params, limit: pageSize }, pageSize)) { | |
out.push(...page); | |
} | |
return out; | |
} | |
async getAllMarkets(params?: Params, pageSize = 100): Promise<MarketLite[]> { | |
const out: MarketLite[] = []; | |
for await (const page of this.paginate<MarketLite>("/markets", { ...params, limit: pageSize }, pageSize)) { | |
out.push(...page); | |
} | |
return out; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment