defmodule Polymarket.Gamma do @moduledoc """ Client for the Polymarket Gamma REST API (https://gamma-api.polymarket.com). """ alias Polymarket.Http alias Polymarket.Schemas.Event alias Polymarket.Schemas.Market alias Polymarket.Schemas.Tag require Logger @url "https://gamma-api.polymarket.com" @typedoc """ Query options forwarded to the Gamma API as query parameters. * `:include_tag` - when `true`, includes the market's `tags` in the response. """ @type option :: {:include_tag, boolean()} @type options :: [option()] @doc """ Retrieves a single market by its unique ID. Returns detailed information about a market. ## Options `opts` are sent verbatim as query parameters. The Gamma API supports: * `:include_tag` - when `true`, includes the market's `tags` in the response. ## Examples Polymarket.Gamma.get_market_by_id(2_691_932, include_tag: true) """ @spec get_market_by_id(non_neg_integer() | String.t(), options()) :: {:ok, Market.t()} | {:error, :get_market_failed} def get_market_by_id(market_id, opts \\ []) do with {:ok, raw} <- Http.get("#{@url}/markets/#{market_id}", opts, __MODULE__), {:ok, market} <- Market.from_attrs(raw) do {:ok, market} else _err -> {:error, :get_market_failed} end end @doc """ Retrieves a single market by its unique slug. Returns detailed information about a market. ## Options `opts` are sent verbatim as query parameters. The Gamma API supports: * `:include_tag` - when `true`, includes the market's `tags` in the response. ## Examples Polymarket.Gamma.get_market_by_slug("sol-updown-5m-1782566100", include_tag: true) """ @spec get_market_by_slug(String.t(), options()) :: {:ok, Market.t()} | {:error, :get_market_failed} def get_market_by_slug(slug, opts \\ []) do with {:ok, raw} <- Http.get("#{@url}/markets/slug/#{slug}", opts, __MODULE__), {:ok, market} <- Market.from_attrs(raw) do {:ok, market} else _err -> {:error, :get_market_failed} end end @doc """ Retrieves the tags attached to a market by the market's unique ID. Returns the list of `Polymarket.Schemas.Tag` structs for the market. ## Examples Polymarket.Gamma.get_market_tags(2_691_932) """ @spec get_market_tags(non_neg_integer() | String.t()) :: {:ok, [Tag.t()]} | {:error, :get_market_tags_failed} def get_market_tags(market_id) do with {:ok, raw} when is_list(raw) <- Http.get("#{@url}/markets/#{market_id}/tags", [], __MODULE__), {:ok, tags} <- parse_tags(raw) do {:ok, tags} else _err -> {:error, :get_market_tags_failed} end end @doc """ Retrieves a single event by its unique ID using `GET /events/{id}`. Returns the parsed `Polymarket.Schemas.Event`, including its nested `markets`, `series`, `tags`, and `event_metadata`. ## Options `opts` are sent verbatim as query parameters. The Gamma API supports: * `:include_chat` - when `true`, includes the event's chat channels. * `:include_template` - when `true`, includes the event's templates. ## Examples Polymarket.Gamma.get_event_by_id(2890) """ @spec get_event_by_id(non_neg_integer() | String.t(), keyword()) :: {:ok, Event.t()} | {:error, :get_event_failed} def get_event_by_id(event_id, opts \\ []) do with {:ok, raw} <- Http.get("#{@url}/events/#{event_id}", opts, __MODULE__), {:ok, event} <- Event.from_attrs(raw) do {:ok, event} else _err -> {:error, :get_event_failed} end end @doc """ Retrieves a single event by its unique slug using `GET /events/slug/{slug}`. Returns the parsed `Polymarket.Schemas.Event`, including its nested `markets`, `series`, `tags`, and `event_metadata`. ## Options `opts` are sent verbatim as query parameters. The Gamma API supports: * `:include_chat` - when `true`, includes the event's chat channels. * `:include_template` - when `true`, includes the event's templates. ## Examples Polymarket.Gamma.get_event_by_slug("nba-mavericks-grizzlies-2021-12-04") """ @spec get_event_by_slug(String.t(), keyword()) :: {:ok, Event.t()} | {:error, :get_event_failed} def get_event_by_slug(slug, opts \\ []) do with {:ok, raw} <- Http.get("#{@url}/events/slug/#{slug}", opts, __MODULE__), {:ok, event} <- Event.from_attrs(raw) do {:ok, event} else _err -> {:error, :get_event_failed} end end @doc """ Retrieves the tags attached to an event by the event's unique ID using `GET /events/{id}/tags`. Returns the list of `Polymarket.Schemas.Tag` structs for the event. ## Examples Polymarket.Gamma.get_event_tags(2890) """ @spec get_event_tags(non_neg_integer() | String.t()) :: {:ok, [Tag.t()]} | {:error, :get_event_tags_failed} def get_event_tags(event_id) do with {:ok, raw} when is_list(raw) <- Http.get("#{@url}/events/#{event_id}/tags", [], __MODULE__), {:ok, tags} <- parse_tags(raw) do {:ok, tags} else _err -> {:error, :get_event_tags_failed} end end @doc """ Fetches a single page of events using the keyset (cursor) pagination endpoint `GET /events/keyset`. Returns the parsed events plus the `next_cursor` to pass as `:after_cursor` on the following call. `next_cursor` is `nil` on the last page. To page through *all* events, prefer `stream_events/1`. ## Options `opts` are sent verbatim as query parameters. The keyset endpoint supports many filters; the most useful are: * `:limit` - max results per page (1..500, default 20) * `:after_cursor` - the `next_cursor` from a previous response * `:order` / `:ascending` - sort field and direction * `:closed` / `:active` / `:archived` / `:featured` - status filters * `:tag_id` / `:tag_slug` - tag filters The `:offset` parameter is rejected by the endpoint; use `:after_cursor`. ## Examples Polymarket.Gamma.list_events(limit: 100, closed: false) """ @spec list_events(keyword()) :: {:ok, %{events: [Event.t()], next_cursor: String.t() | nil}} | {:error, :list_events_failed} def list_events(opts \\ []) do with {:ok, %{events: raw} = body} when is_list(raw) <- Http.get("#{@url}/events/keyset", opts, __MODULE__), {:ok, events} <- parse_events(raw) do {:ok, %{events: events, next_cursor: Map.get(body, :next_cursor)}} else _err -> {:error, :list_events_failed} end end @doc """ Returns a lazy `Stream` of every event matching `opts`, transparently following the keyset cursor across pages. Each element is a `Polymarket.Schemas.Event`. The stream fetches one page at a time as it is consumed, so it is safe to use over the full (large) event set, e.g. with `Stream.take/2`. Materialise the whole result with `Enum.to_list/1`. `opts` are forwarded to `list_events/1` (an `:after_cursor` in `opts` is used as the starting page). A larger `:limit` reduces the number of round-trips. Raises if a page fails to fetch, so that a partial result is never silently mistaken for the complete set. ## Examples # All open events. Polymarket.Gamma.stream_events(closed: false) |> Enum.to_list() # Just the first 50, without fetching everything. Polymarket.Gamma.stream_events(limit: 100) |> Enum.take(50) """ @spec stream_events(keyword()) :: Enumerable.t() def stream_events(opts \\ []) do base = Keyword.delete(opts, :after_cursor) Stream.resource( fn -> {:fetch, Keyword.get(opts, :after_cursor)} end, fn :halt -> {:halt, nil} {:fetch, cursor} -> page_opts = if cursor, do: Keyword.put(base, :after_cursor, cursor), else: base case list_events(page_opts) do {:ok, %{events: events, next_cursor: nil}} -> {events, :halt} {:ok, %{events: events, next_cursor: next}} -> {events, {:fetch, next}} {:error, reason} -> raise "Polymarket.Gamma.stream_events failed: #{inspect(reason)}" end end, fn _acc -> :ok end ) end # ---------------------------------------------------------------------------# # Helpers # # ---------------------------------------------------------------------------# # Parses each element of `raw` with `parse_fun`, accumulating in order. When # `ignore_errors?`, invalid elements are skipped; otherwise it fails fast on # the first invalid element. @spec parse_list([map()], (map() -> {:ok, item} | {:error, {:missing_fields, [atom()]}}), boolean()) :: {:ok, [item]} | {:error, {:missing_fields, [atom()]}} when item: var defp parse_list(raw, parse_fun, ignore_errors?) do raw |> Enum.reduce_while({:ok, []}, fn attrs, {:ok, acc} -> case parse_fun.(attrs) do {:ok, item} -> {:cont, {:ok, [item | acc]}} {:error, _reason} when ignore_errors? -> {:cont, {:ok, acc}} {:error, _reason} = error -> {:halt, error} end end) |> case do {:ok, items} -> {:ok, Enum.reverse(items)} error -> error end end # The keyset feed occasionally carries a malformed event; skip those rather # than fail the whole page. @spec parse_events([map()]) :: {:ok, [Event.t()]} | {:error, {:missing_fields, [atom()]}} defp parse_events(raw), do: parse_list(raw, &Event.from_attrs/1, true) # Tags are few and expected to be well-formed, so fail fast on any invalid one. @spec parse_tags([map()]) :: {:ok, [Tag.t()]} | {:error, {:missing_fields, [atom()]}} defp parse_tags(raw), do: parse_list(raw, &Tag.from_attrs/1, false) end