← Back to blog

Published

How to Build a Polymarket Bot with Python and Live News

A useful Polymarket bot needs two different views of an event. Polymarket tells the bot what traders are currently willing to pay. Fresh public-web evidence tells it what may have changed outside the market.

This guide builds that research loop in Python. It reads a live Polymarket order book, searches recent Google News results through PrismCrawl, removes duplicate sources, and creates a paper signal for review. It does not promise profitable trades, and the example never submits an order.

What this Polymarket bot does

Polymarket Gamma API  -> question, rules, outcome token
Polymarket CLOB API   -> executable YES bids and asks
PrismCrawl API        -> fresh, source-linked Google News results
Your policy code      -> verify, compare, log, and alert
Optional executor     -> trade only after independent safety checks

Polymarket separates discovery data from order-book data. Its market-data overview explains that events contain one or more markets, each outcome has a token ID, and the token ID is used to read prices or an order book. Public market data does not require a wallet or API key.

PrismCrawl has a different job. It runs a live Google or Bing search and returns ranked URLs, titles, snippets, domains, and SERP features in a documented JSON shape. Search results help the bot discover evidence. They are not a substitute for reading the underlying source.

Benefits of this design

  • Broader event coverage: One scheduler can watch policy announcements, court decisions, elections, company statements, and other events that surface on the public web.
  • Explainable alerts: Every signal can retain its market question, price snapshot, source URLs, and PrismCrawl request IDs.
  • Less alert noise: URL and domain deduplication can suppress repeated coverage of the same underlying report.
  • Safer iteration: Research, paper signals, and order execution remain separate, so the bot can prove its data quality before money is at risk.
  • Simple search costs: Each successful PrismCrawl search consumes one credit, while failed searches consume none.

Step 1: Find the exact Polymarket contract

Do not select a contract from its short title alone. Save the full question, description, end date, resolution source, outcomes, and token IDs. Two similarly worded markets can settle under different rules.

The example below uses Polymarket's public search endpoint, keeps active order-book markets, and selects the most liquid match for inspection. A production system should require a human-approved market ID instead of automatically trusting the first search result.

import json
from dataclasses import dataclass
from decimal import Decimal

import requests

GAMMA_URL = "https://gamma-api.polymarket.com"
CLOB_URL = "https://clob.polymarket.com"


@dataclass(frozen=True)
class PolymarketSnapshot:
    question: str
    condition_id: str
    yes_token_id: str
    resolution_source: str | None
    best_yes_bid: Decimal
    best_yes_ask: Decimal

    @property
    def midpoint(self) -> Decimal:
        return (self.best_yes_bid + self.best_yes_ask) / Decimal("2")


def decode_list(value: str | list[str]) -> list[str]:
    return value if isinstance(value, list) else json.loads(value)


def find_market(topic: str) -> dict:
    response = requests.get(
        f"{GAMMA_URL}/public-search",
        params={"q": topic, "limit_per_type": 5, "search_profiles": False},
        timeout=20,
    )
    response.raise_for_status()

    candidates = [
        market
        for event in response.json().get("events") or []
        for market in event.get("markets") or []
        if market.get("active")
        and not market.get("closed")
        and market.get("enableOrderBook")
    ]
    if not candidates:
        raise LookupError(f"No active Polymarket contract found for {topic!r}")

    return max(candidates, key=lambda market: float(market.get("liquidityNum") or 0))


def read_snapshot(topic: str) -> PolymarketSnapshot:
    market = find_market(topic)
    outcomes = decode_list(market["outcomes"])
    token_ids = decode_list(market["clobTokenIds"])
    tokens_by_outcome = dict(zip(outcomes, token_ids, strict=True))
    yes_token_id = tokens_by_outcome["Yes"]

    response = requests.get(
        f"{CLOB_URL}/book",
        params={"token_id": yes_token_id},
        timeout=20,
    )
    response.raise_for_status()
    book = response.json()

    if not book["bids"] or not book["asks"]:
        raise ValueError("The selected YES token does not have a two-sided order book")

    return PolymarketSnapshot(
        question=market["question"],
        condition_id=market["conditionId"],
        yes_token_id=yes_token_id,
        resolution_source=market.get("resolutionSource") or None,
        best_yes_bid=Decimal(book["bids"][0]["price"]),
        best_yes_ask=Decimal(book["asks"][0]["price"]),
    )

The current Polymarket public-search documentation defines the event and market fields used above. The CLOB order-book endpoint returns bids, asks, sizes, the minimum order size, and the tick size for a token.

Use bids and asks instead of treating the last trade or displayed outcome price as immediately executable. A 60 percent midpoint with a wide spread is a different opportunity from a tight 60 percent market.

Step 2: Search for recent evidence with PrismCrawl

Keep the PrismCrawl key in a server-side environment variable. The Google News tab is selected with udm: 12, and tbs: "qdr:d" limits the request to recent results. The exact query should reflect the contract's resolution wording.

import os
from urllib.parse import urlsplit

PRISMCRAWL_API_KEY = os.environ["PRISMCRAWL_API_KEY"]
PRISMCRAWL_URL = "https://api.prismcrawl.com/v1/google/search"


def search_recent_news(question: str) -> list[dict]:
    response = requests.post(
        PRISMCRAWL_URL,
        headers={"x-api-key": PRISMCRAWL_API_KEY},
        json={
            "query": question,
            "gl": "us",
            "hl": "en-US",
            "udm": 12,
            "tbs": "qdr:d",
        },
        timeout=60,
    )
    response.raise_for_status()
    body = response.json()
    if not body["success"]:
        raise RuntimeError(body["error"]["message"])

    request_id = body["request_id"]
    results = body["data"]["content"]["results"]
    return [
        {
            "request_id": request_id,
            "rank": item["rank"],
            "title": item["title"],
            "url": item["url"],
            "domain": urlsplit(item["url"]).hostname or item.get("domain"),
            "snippet": item.get("snippet"),
        }
        for item in results
        if item.get("url")
    ]

The response path and fields match the public PrismCrawl API reference. To compare coverage, run the same query through /v1/microsoft/search and merge by destination URL. Two search engines pointing to one article still count as one source.

Step 3: Remove duplicate coverage

News syndication can make one report look like ten independent confirmations. Start by normalizing URLs and limiting each domain. A stronger version also tracks canonical URLs and identifies articles that quote the same original statement.

from urllib.parse import urlsplit, urlunsplit


def normalized_url(raw_url: str) -> str:
    parts = urlsplit(raw_url)
    return urlunsplit((parts.scheme, parts.netloc.lower(), parts.path.rstrip("/"), "", ""))


def unique_evidence(items: list[dict], per_domain: int = 2) -> list[dict]:
    seen_urls: set[str] = set()
    domain_counts: dict[str, int] = {}
    output: list[dict] = []

    for item in items:
        url = normalized_url(item["url"])
        domain = (item["domain"] or "unknown").removeprefix("www.")
        if url in seen_urls or domain_counts.get(domain, 0) >= per_domain:
            continue
        seen_urls.add(url)
        domain_counts[domain] = domain_counts.get(domain, 0) + 1
        output.append({**item, "url": url, "domain": domain})

    return output

Deduplication is only the first filter. Before assigning probability, open the selected sources and check the publication time, author, quoted document, and whether the story actually addresses the settlement condition.

Step 4: Produce a paper signal

The research probability below is an input from a separately tested process. It must be based on verified source content, not a count of positive words in snippets. The function rejects wide spreads, single-domain stories, and small differences.

def build_review_alert(
    snapshot: PolymarketSnapshot,
    research_probability: Decimal,
    evidence: list[dict],
) -> dict | None:
    if not Decimal("0") <= research_probability <= Decimal("1"):
        raise ValueError("research_probability must be between 0 and 1")

    spread = snapshot.best_yes_ask - snapshot.best_yes_bid
    independent_domains = {item["domain"] for item in evidence}
    difference = research_probability - snapshot.midpoint

    if spread > Decimal("0.08"):
        return None
    if len(independent_domains) < 2:
        return None
    if abs(difference) < Decimal("0.10"):
        return None

    return {
        "action": "human_review",
        "question": snapshot.question,
        "condition_id": snapshot.condition_id,
        "best_yes_bid": str(snapshot.best_yes_bid),
        "best_yes_ask": str(snapshot.best_yes_ask),
        "research_probability": str(research_probability),
        "difference_from_midpoint": str(difference),
        "source_urls": [item["url"] for item in evidence[:5]],
        "prismcrawl_request_ids": sorted({item["request_id"] for item in evidence}),
    }

The eight-point spread and ten-point difference are illustrative thresholds, not strategy recommendations. Backtests must account for when each source became available, the contemporaneous order book, fees, slippage, fills, and markets that never produced a signal.

Step 5: Treat execution as a separate system

Do not add private keys or order credentials to the research process. A production executor should accept a small, validated order intent and independently enforce:

  • market and token allowlists;
  • maximum order size and total exposure;
  • stale-price and stale-evidence cutoffs;
  • price limits and minimum liquidity;
  • duplicate-order protection;
  • a kill switch and an append-only audit log.

Polymarket's production order interface is CLOB V2. Use the current official order documentation or maintained client rather than copying an old signing example. Confirm that the service is available to you and that your use complies with the current terms before enabling any trade.

When a Polymarket news bot helps

This pattern works best when a public document or announcement can change the market's outlook: agency action, legislation, court filings, company releases, scheduled economic reports, and election administration updates.

It is a poor substitute for an authoritative low-latency feed. A sports score, exchange price, or weather observation should come directly from the relevant official or licensed data source. PrismCrawl adds public-web discovery and context around the market. It should not be the price feed or the final judge of truth.

Start with one approved contract, store every input, and run the bot in observation mode. Once the alerts are consistently timely, relevant, and reproducible, move to paper trading. Only measured results should determine whether automatic execution deserves consideration.

Create a PrismCrawl account to test the news-discovery request, or read the broader guide to prediction-market research bots before choosing a venue.

Frequently asked questions

Can PrismCrawl place trades on Polymarket?

No. PrismCrawl supplies live Google and Bing search results for the research layer. Polymarket market data and any deliberately enabled order submission use Polymarket's official APIs.

Is a Polymarket price a guaranteed probability?

No. A market price reflects current trading and can be affected by spread, liquidity, fees, and participant beliefs. Use executable bids and asks, then evaluate evidence against the contract's exact resolution rules.

Should a Polymarket bot trade directly from news snippets?

No. Search snippets are discovery signals. Open and verify the underlying sources, test the strategy with historical data and paper trading, and keep deterministic limits between research and execution.