Skip to content

Instantly share code, notes, and snippets.

@chand1012
Created October 13, 2025 18:01
Show Gist options
  • Save chand1012/54d98e2f78a6ffa90991d5f586364b6d to your computer and use it in GitHub Desktop.
Save chand1012/54d98e2f78a6ffa90991d5f586364b6d to your computer and use it in GitHub Desktop.
Single-file typed Polymarket Gamma API
// 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