How to Build a Polymarket Trading Bot (2026)
A line-by-line Python guide to building a Polymarket trading bot in 2026, from free keyless market data to the honest cost of placing your first order.
The Short Answer
A Polymarket trading bot is a Python script that reads market data from Polymarket's public API, applies a rule you wrote, and optionally signs orders from your own wallet. The reading half is free and keyless: Gamma lists markets, the CLOB (the central limit order book, Polymarket's matching engine) serves prices, books and history, and both answered unauthenticated requests on 2026-08-07. The writing half needs a funded Polygon wallet, pUSD collateral (Polymarket's dollar-pegged token) and a two layer signature scheme, which is where most beginners stop. Build the read path first, then add execution last and small.
This is a build guide for a Polymarket trading bot, aimed at someone who can run a Python script but has never touched a prediction market API. Order book, ticker, maker, taker, websocket and rate limit are explained as they become load-bearing. Every code block below appends to the same file and relies on the variables defined above it, so work through them in order rather than starting a fresh script per step. By the end you will have a script that finds a live market, resolves its outcome tokens, reads a price correctly, pulls history, polls without getting throttled, streams updates, and spends nothing until you let it.
One warning first. Nearly every Polymarket bot tutorial online predates April 2026 and will not run. Polymarket cut over to a V2 order book on 2026-04-28 with, in its own docs' words, no backward compatibility, and the Python SDK has been replaced twice since. Every URL, parameter and field name below was checked against a live response on 2026-08-07. Rate limits, defaults and SDK versions move, though, so confirm anything load-bearing against Polymarket's own docs before you build on it.
Key Takeaways
- Reading Polymarket costs nothing and needs no key: Gamma for discovery, CLOB for prices, books and history. Funds matter only once you want to place an order.
- The bug that catches almost everyone:
clobTokenIds,outcomesandoutcomePricesarrive as JSON-encoded strings, not arrays, whilebestBidandbestAskon the same object are plain numbers. - A sandbox is a fake-money copy of an exchange, same endpoints and same responses but no real funds, and it is where you would normally test a bot. Polymarket does not offer one, so testing means real pUSD (its dollar-pegged collateral token) on mainnet. A dry-run flag, a position cap and a kill switch are not polish you add later, they are your test environment.
What a Polymarket Trading Bot Actually Talks To
A bot is a loop: read the state, apply a rule, act or do nothing, wait, repeat. The API half is an afternoon of work; the rule in the middle is the part nobody can hand you, so treat all of this as plumbing rather than strategy.
Polymarket is not one API, it is four. A market is a question with a fixed set of outcomes, usually Yes and No. Each outcome has its own token id, a very long number naming the tradeable thing, while the market as a whole has a conditionId. They are not interchangeable: every price, book and history call wants the token id, and passing a condition id is the most common cause of a mysterious 404.
Prices run from $0.00 to $1.00 and read as probability, so $0.25 means roughly a 25% chance, and Yes and No sum to $1.00. If that is new, our explainer on how Polymarket works covers the concepts without code.
| API | Subdomain | What a bot uses it for | Credentials |
|---|---|---|---|
| Gamma | gamma-api | Finding events and markets, metadata, token ids | None |
| CLOB | clob | Prices, order books, history, placing orders | None to read, signatures to trade |
| Data | data-api | Positions, activity, participation | None documented |
| Relayer | relayer-v2 | Wallet transactions without holding POL for gas | Relayer key headers |
Each subdomain above is followed by .polymarket.com, so the CLOB lives at https://clob.polymarket.com. One honest note on the Data API row: its positions reference publishes no security scheme, and a live call returned 200 with no credentials on 7 August 2026. That is an absence of documentation rather than a promise, so confirm it yourself before building anything that depends on staying keyless.
Step 1: Find a Live Market and Print It
No account, no key, no funds. Two dependencies cover the whole guide: pip install requests "websockets>=12". Nothing before Step 3 touches the second one. Save this as bot.py and run it. It asks Gamma for the highest-volume open market and prints what came back.
import requests
GAMMA = "https://gamma-api.polymarket.com"
resp = requests.get(
f"{GAMMA}/markets",
params={"limit": 1, "closed": "false", "order": "volumeNum", "ascending": "false"},
timeout=10,
)
resp.raise_for_status()
market = resp.json()[0]
print(market["question"])
print(market["bestBid"], market["bestAsk"], market["lastTradePrice"])
import requestsis the only non-standard-library import until the websocket in Step 3, which addswebsockets. Polymarket's read endpoints are plain HTTP and JSON, so an SDK here would only hide the parts you are trying to learn.GAMMA = "https://gamma-api.polymarket.com"points at the discovery API, the only one that can tell you which markets exist at all. It is public: no signup, no key, no identity attached to your rate limit.params={"limit": 1, ...}asks for one result. Raise it once you trust the shape, but start at 1 so the response is small enough to read with your eyes."closed": "false"excludes markets that have already resolved, and the lowercase string is deliberate.requestsputs a PythonFalseon the wire as the literalFalse, capital F, which no API is obliged to read as a boolean. Should the filter quietly fail, you get the highest-volume resolved market instead, and then every price call in Step 2 404s while you debug the wrong step. Write query-string booleans as strings. Excluding closed markets has been the server-side default since 2026-04-09, but saying it out loud means your script keeps behaving the same way if that default moves again."order": "volumeNum", "ascending": "false"sorts by traded volume, biggest first. Leave it out and you get an effectively arbitrary market, usually one nobody trades, and every price you print afterwards will look broken when it is merely dead.timeout=10matters more than it looks. With no timeoutrequestswaits forever, so one stalled socket freezes your bot permanently without ever raising an error you would see in a log.resp.raise_for_status()turns a 429 or a 500 into an exception right here. Without it an error page parses as JSON perfectly happily, then explodes thirty lines later as aKeyErrorand sends you debugging the wrong function.resp.json()[0]indexes into a list because this endpoint always returns an array, even when you asked for a single item. There is no wrapper object to reach through.market["bestBid"]is the highest price anyone is currently willing to pay you, andbestAskis the lowest price anyone will sell to you at. Those two, plus every order queued behind them, are what people mean by the order book. Gamma hands you both as ordinary numbers, so this script sees a real price without a second call.
Now the trap. Polymarket's own documentation flags this one, which tells you how often it catches people. Append it to bot.py underneath what you already have; it needs the market object from above.
import json
token_ids = json.loads(market["clobTokenIds"])
outcomes = json.loads(market["outcomes"])
print("condition:", market["conditionId"])
for name, token_id in zip(outcomes, token_ids):
print(f"{name:>4}: {token_id}")
json.loads(market["clobTokenIds"])is mandatory, not defensive. That field arrives as a JSON-encoded string that looks like"[\"27146...\", \"33216...\"]", so indexing it without parsing hands you a single character rather than a token id, and the character is a square bracket.json.loads(market["outcomes"])is the same story for the labels, andoutcomePricesis stringified too. What makes this genuinely nasty is thatbestBid,bestAskandlastTradePriceon the same object are bare numbers. A blanket rule like "Gamma returns strings" will not save you. Check each field you touch.market["conditionId"]identifies the market as a whole. You need it if you ever subscribe to the authenticated user websocket channel, and never for a price lookup. Keep the two ids in separate variables so you cannot fat-finger one into the other.zip(outcomes, token_ids)works because the two arrays are positionally aligned: element 0 ofoutcomesbelongs to element 0 ofclobTokenIds. That pairing is how you learn which enormous number means Yes.f"{name:>4}"right-aligns the label so Yes and No line up in the terminal. Purely cosmetic, but you will be staring at this output for a while.
Step 2: Read a Price the Bot Can Trust
The CLOB, the matching engine holding every resting order, has public read endpoints too. There is no single ticker on Polymarket, no one lightweight last-price feed the way an equities API gives you. There are several price endpoints instead, and they disagree by design.
CLOB = "https://clob.polymarket.com"
def get_price(token_id, side="BUY"):
r = requests.get(
f"{CLOB}/price",
params={"token_id": token_id, "side": side},
timeout=10,
)
r.raise_for_status()
return float(r.json()["price"])
for name, token_id in zip(outcomes, token_ids):
p = get_price(token_id)
print(f"{name:>4}: ${p:.3f} ({p * 100:.1f}% implied)")
CLOB = "https://clob.polymarket.com"is a different host from Gamma with its own separate rate-limit budget, so hammering prices does not eat into your market-discovery allowance.params={"token_id": token_id, "side": side}requires both parameters.sidetakes exactlyBUYorSELLin capitals, and the two answer different questions: what it costs you to buy this outcome now, versus what you would receive selling it now.side="BUY"is the default, so the loop underneath prints what it would cost to buy each outcome. Both numbers are asks, which means Yes plus No comes to a little over $1.00, something like 0.63 and 0.39. That gap is the spread, not a bug in your code. Passside="SELL"and the pair lands slightly under $1.00 instead. The tidy $1.00 identity from earlier holds at the midpoint, not at the price you actually trade.float(r.json()["price"])is not optional. The value arrives quoted, as{"price":"0.005"}rather than a bare number, and the reference page now describes price, midpoint and spread alike as decimal strings. Compare that string against a float and Python raises; sort by it and you get alphabetical nonsense where you expected numeric order.r.raise_for_status()earns its place here specifically. A wrong or stale token id comes back as 404 with the messageNo orderbook exists for the requested token id, which is the clearest possible sign you passed a condition id where a token id belonged.p * 100converts the price into implied probability. It is the same number in different clothes, but thinking in percent makes an absurd signal obvious about ten times faster than thinking in dollars.
Three siblings are worth knowing. /midpoint returns the halfway point between best bid and best ask. At the time of writing it answers under the key mid, quoted like /price, so wrap it in float() the same way. The prices and order books reference describes these values as decimal strings but prints no sample response, so print the raw JSON once yourself before you index into it. /last-trade-price quietly returns "0.5" when a market has never traded, a default masquerading as a coin flip. /spread gives the gap between bid and ask. One quirk: the website shows the midpoint unless the spread exceeds $0.10, where it shows the last trade instead, so /midpoint can legitimately disagree with the browser.
History is one endpoint away, and it hides two naming traps in a single call.
import time
r = requests.get(
f"{CLOB}/prices-history",
params={"market": token_ids[0], "interval": "1d", "fidelity": 60},
timeout=10,
)
r.raise_for_status()
hist = r.json()["history"]
for point in hist[-5:]:
stamp = time.strftime("%H:%M", time.localtime(point["t"]))
print(stamp, f'{point["p"]:.4f}')
token_ids[0]is whichever outcome printed first in the block above, usually Yes, but that ordering is not a promise the API makes. Once you have run the zip once, pin it by name instead:yes_id = dict(zip(outcomes, token_ids))["Yes"], then use that variable everywhere below."market": token_ids[0]is trap one. The parameter is namedmarket, but it wants the CLOB token id, the same long number you just used for/price. Pass the actualconditionIdand the call comes back as an HTTP error rather than history, which is a confusing way to learn that the name is misleading.r.raise_for_status()sits on its own line here because the request and the parse are now two statements. Chain.json()["history"]straight onto the request instead and that misleadingmarketparameter surfaces asKeyError: 'history', three functions away from the thing you actually got wrong."interval": "1d"is the lookback window, and the allowed values aremax,1m,1w,1d,6hand1h. Trap two lives in that list:1mmeans one month, not one minute. For an absolute range instead, passstartTsandendTsin unix seconds."fidelity": 60is the candle resolution in minutes and defaults to 1. Interval and fidelity are separate ideas: one sets how far back you look, the other how coarse the points are. A full day at the default fidelity is 1,440 points, most of which you will never read..json()["history"]unwraps the envelope. Each entry is{"t": 1786017622, "p": 0.012}, oldest first, and note thatphere is a bare float while/pricegave you a string for the same concept. No maximum row count is documented, so do not assume one and do not assume there is no ceiling either.time.localtime(point["t"])works becausetis unix seconds. Feed a millisecond timestamp into the same function elsewhere in your code and you will render dates in the year 58000 and blame the exchange.
Step 3: Poll on a Timer, Then Stream
Polling means asking again on a schedule, and it is the right first loop because it fails in readable ways. A rate limit is the venue capping how often you may ask. On 2026-08-09 the CLOB allowed roughly 1,500 requests per 10 seconds each to /price, /book and /midpoint, while Gamma's /markets was tighter at 300 per 10 seconds (Polymarket's rate limits page; check it before you tune a loop instead of trusting a figure in a blog post). Those public read limits are IP-based and enforced through Cloudflare, so going over normally delays and queues your requests rather than rejecting them. The documented 429, its Retry-After header and the Poly-RateLimit-* headers belong to the per-signer trading limits you meet once you are placing orders. Handle the 429 anyway; it costs four lines and it covers you when a limit changes shape.
One thing about the block below: it runs until you stop it. Run it on its own first, then comment out the while True: loop before you append the next two blocks, or nothing after it in bot.py will ever execute.
POLL_SECONDS = 15
while True:
try:
price = get_price(token_ids[0])
except requests.HTTPError as exc:
if exc.response.status_code == 429:
time.sleep(float(exc.response.headers.get("Retry-After", 5)))
continue
raise
print(time.strftime("%H:%M:%S"), f"{price:.3f}")
time.sleep(POLL_SECONDS)
POLL_SECONDS = 15is deliberately lazy. One market every 15 seconds is nowhere near any published limit, and starting slow means your first bug is almost certainly a logic bug rather than a throttling problem you cannot see.while True:has no exit inside it. Ctrl-C is the only way out, and anything you append below this loop is unreachable until you comment the loop out or move it behindif __name__ == "__main__":.except requests.HTTPError as exccatches only whatraise_for_status()raises. Dropped connections and DNS failures are different exception classes and will still kill this loop, which for a first bot is the honest outcome: better a loud crash than a bot you merely believe is running.exc.response.status_code == 429singles out "too many requests". Two neighbours deserve the same handling once you go beyond this example:425 Too Earlymeans the matching engine is restarting, and503means the exchange is paused, in post-only mode, or in cancel-only mode. That last one is the case a running bot most needs to tell apart: your cancels still work, your new orders do not.headers.get("Retry-After", 5)obeys the wait the server asked for, falling back to five seconds when the header is absent.Poly-RateLimit-RemainingandPoly-RateLimit-Resetare worth logging once you are trading, but they are documented on the authenticated trading limits, so do not expect to see them on a public price read like this one and do not conclude your logging is broken when they are missing.continueretries without also sleeping the full poll interval on top of the backoff. Be aware this is a flat retry, not exponential backoff, which means doubling the wait after each consecutive failure: 5 seconds, then 10, then 20, capped at a minute. The docs ask for that, and anything running unattended should do it instead of hammering at a fixed rate.time.sleep(POLL_SECONDS)at the bottom of the loop is this script's entire rate-limit strategy. Delete that one line and you will send thousands of requests a second, which is how a first bot gets throttled inside a minute.
Polling shows you the price after it moved. A websocket is a connection that stays open so the server can push updates the instant they happen instead of you asking. Polymarket's market data channel needs no authentication, which makes it a gentle place to learn. This is where the second dependency arrives: pip install "websockets>=12", since the .sync client below does not exist in older releases.
import threading
from websockets.sync.client import connect # needs websockets 12 or newer
WS = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
with connect(WS) as ws:
ws.send(json.dumps({"assets_ids": [token_ids[0]], "type": "market"}))
def keepalive():
while True:
time.sleep(10)
ws.send("PING")
threading.Thread(target=keepalive, daemon=True).start()
for message in ws:
print(message[:120])
WS = "wss://ws-subscriptions-clob.polymarket.com/ws/market"is the public market data channel. There is a separate/ws/userchannel for your own orders and fills, and that one does need credentials, sent inside the first message body rather than as HTTP headers.{"assets_ids": [...], "type": "market"}is the subscribe message, and both fields are required. Read the spelling twice:assets_ids, plural on both words. It looks like a typo in the docs. It is not, and getting it wrong gives you a connection that opens perfectly and then sends you nothing forever."type": "market"has to be that literal string. Optional extras include"initial_dump": true, which sends a snapshot of the current book before live updates begin and is already the default, and"level", which controls detail and defaults to 2.ws.send("PING")every 10 seconds is the requirement people miss most often. It is an application-level text frame containing those four characters, and the server repliesPONG. The automatic ping your websocket library sends at the protocol level does not count, so a bot without this line dies quietly after a while and looks exactly like a flaky network.threading.Thread(..., daemon=True)runs the keepalive next to the read loop, anddaemon=Truelets the program exit without waiting on that infinite loop. This is the shortest way to demonstrate the point, and it is fine for watching a stream in your own terminal. Before you leave it running unattended, move the ping and the reads onto two asyncio tasks, because sending from a second thread is not something the sync client promises to handle; the asyncio quickstart in thewebsocketsdocs has the pattern more or less ready to paste.for message in wsblocks until the server pushes something, then hands it to you. That is the actual difference from polling: your code runs when the market moves, not when your timer happens to fire. Like the poller above, it never returns on its own, so comment this block out too before you append Step 4.message[:120]truncates the output. Full book updates are long, and an untruncated stream fills a terminal faster than you can read one line of it.
Step 4: What Placing an Order Actually Costs You
Everything above was free. This part is not. You need a Polygon wallet and pUSD collateral, Polymarket's ERC-20 wrapper around USDC with 6 decimals, which replaced USDC.e at the V2 cutover. Polymarket's trading quickstart recommended keeping at least 10 pUSD available when I checked it on 2026-08-09. A balance check written against USDC reports zero, so an otherwise correct bot decides it is unfunded.
Authentication comes in two layers and the SDK implements both, so you will not be writing either by hand. L1 is a one-time signature from your wallet proving you own the address; its whole job is handing you an API key and secret. L2 is a per-request fingerprint computed from that secret, which is how the exchange knows a given order really came from you. If you ever do implement it yourself, the details are these: L1 is EIP-712, L2 is HMAC-SHA256 over timestamp plus method plus path plus body with nothing between them, the secret is base64-decoded before it is used as the key, and timestamps are unix seconds rather than milliseconds. Everyone else runs pip install polymarket-client, then import polymarket, because the names differ. It wants Python 3.11 or newer and was at 0.5.0 on 2026-08-09 (PyPI). Treat that number as a snapshot rather than a fact: 0.3.0, 0.4.0 and 0.5.0 all landed inside the first week of August, and the project's own note is that minor releases on the 0.x line can carry breaking changes. Pin an exact version in your requirements file and read the release notes before you move off it, so a fresh pip install never hands a running bot a different API than the one you tested.
Before you wire any of that in, write the guard rails. The block below reads your wallet private key from an environment variable, and it is worth being blunt about what that key is: whoever holds it holds the funds, permanently, with nobody to appeal to. On Polymarket it comes from the export option in your account settings. Set it for the current terminal with $env:POLYMARKET_PRIVATE_KEY = "0x..." in PowerShell, set POLYMARKET_PRIVATE_KEY=0x... in cmd, or export POLYMARKET_PRIVATE_KEY=0x... on macOS and Linux. None of those survive closing the window; for anything longer-lived put the key in a .env file and add that file to .gitignore first, not after.
import os
PRIVATE_KEY = os.environ.get("POLYMARKET_PRIVATE_KEY")
if not PRIVATE_KEY:
raise SystemExit("Set POLYMARKET_PRIVATE_KEY in your environment first.")
DRY_RUN = True
MAX_ORDER_USD = 5.0
def place(token_id, price, size_usd):
if DRY_RUN:
print(f"[dry-run] buy {size_usd} of {token_id[:8]}... at {price}")
return None
if size_usd > MAX_ORDER_USD:
raise ValueError(f"order {size_usd} exceeds MAX_ORDER_USD={MAX_ORDER_USD}")
raise NotImplementedError("Wire the SDK in here, once the dry-run log looks right.")
os.environ.get("POLYMARKET_PRIVATE_KEY")is the only acceptable pattern for this value. A wallet private key is not a read-only API key, it controls funds outright. Hardcode it and it lands in git history, in your editor's autosave files, and in the first screenshot you paste somewhere asking for help.raise SystemExit(...)fails fast with a message a human can act on, instead of letting aNonekey travel into the SDK and resurface as an unrelated signing error twenty stack frames deep.DRY_RUN = Trueas the default is your kill switch, and the direction matters. Safe is the state you get by forgetting; dangerous has to be chosen on purpose. Make it readable from an environment variable later so you can disarm a running bot without editing code.MAX_ORDER_USD = 5.0caps a single order, and the number is small deliberately. A sizing bug that multiplies where it should divide is a genuinely common mistake, and a cap turns that from a wipeout into a bad five dollars.if DRY_RUN:stands alone, and keeping the size check out of it is the point. Fold the two together and an oversized live order gets skipped but printed as[dry-run], which makes an armed bot and a rehearsing bot look identical in the one log the next section tells you to trust.return Noneforces callers to handle "no order was placed", which is the same shape as a rejected order. Code that assumes an order object always comes back breaks the first time the exchange declines, and it will decline.raise ValueError(...)makes a blown cap loud. A sizing bug should stop the program, not leave a tidy line in a file you will skim.size_usdis dollars, but Polymarket sizes orders in outcome tokens, so the conversion at the SDK boundary issize = size_usd / price. Pastesize=size_usdstraight in and you order that many shares, costing shares times price, after whichMAX_ORDER_USDquietly means something other than dollars.raise NotImplementedError(...)is not laziness on my part. The real SDK call belongs on that line, the order-creation and post pair inpolymarket-client, and our Polymarket API guide walks through a signed example. An explicit crash beats a silent stub that lets you believe you are trading when you are not.
One last piece of vocabulary, because it shows up on your statement. A maker order rests on the book waiting for someone to hit it, adding liquidity; a taker order fills immediately against orders already there. Which one you are affects what you pay, and since the cutover fees are set at match time rather than attached to the order you signed. Our Polymarket fees breakdown has the details.
Why Older Bot Tutorials Break
If you already tried this and gave up, here is the likely reason. The Python SDK has three generations. py-clob-client, which essentially every pre-2026 tutorial imports, sits in an archived repository whose README tells you to leave. py-clob-client-v2 was the interim CLOB V2 client and is itself superseded. polymarket-client is the current unified package. The old line client.set_api_creds(client.create_or_derive_api_creds()) is obsolete, because the new clients derive credentials during construction.
The cutover changed the wire format too. The order struct lost nonce, feeRateBps and taker and gained timestamp, metadata and builder. The EIP-712 exchange domain version went from 1 to 2. Resting orders were wiped, not migrated. Collateral moved from USDC.e to pUSD. Old approval scripts point at a Neg Risk Adapter whose redeem grace period ended on 2026-07-17. Separately, from 2026-07-24, POST /order and POST /orders stopped returning transactionHashes on successful FAK and FOK matches and hand back tradeIDs instead, which you poll to recover the hashes (Polymarket's predictions changelog, checked 2026-08-09). A guide written before all that is not slightly stale, it describes a different exchange. The data calls in our Polymarket API Python tutorial are kept current against live responses.
Before You Risk Real Money
Here is the section most bot tutorials skip. Polymarket has no beginner sandbox, no fake-money mirror of the exchange to rehearse against. A staging host appears in the OpenAPI server list, but no documentation describes a sandbox workflow, no separate-credentials policy is published, and we could not confirm it accepts traffic. Treat it as unavailable: testing here means real money on Polygon mainnet. Kalshi does run a genuine demo environment with its own endpoints and credentials, so if your idea works on either venue that is the cheaper classroom (see the Kalshi API guide).
So the safety work lives in your own code. Run in dry-run mode for weeks, logging every order the bot would have placed with a timestamp, a price and a reason, then score that log against what happened. Most first strategies lose money on paper, which is the cheapest place to find out. Cap total exposure, not just one order. Size against order book depth rather than your balance, because a thin book means your own order pushes the price against you. Build a kill switch you can trigger in one command, and test it before you need it.
Plainly: none of this is financial advice, an automated strategy is not a safer strategy, and a bot's core talent is repeating your mistake faster than you can notice. Start with an amount you would shrug off.
Frequently Asked Questions
How do I build a Polymarket trading bot?
Start with the read side, which needs no account: call the Gamma API for a market, parse clobTokenIds into token ids, then ask the CLOB for a price. Wrap that in a loop, add a rule, and wire in order placement last. Early bugs then cost you time, not money.
Can you use bots on Polymarket?
Yes. Polymarket publishes API documentation, maintains an official Python SDK, and runs trading rate-limit tiers that scale with your 30-day volume, which is not what a venue does when it wants automation kept out. Much of the resting liquidity was placed by software.
Is Polymarket bot trading allowed?
Programmatic trading is a supported use of the API, not a workaround: the documented rate limits start with a Standard tier requiring no volume history, which is where a new bot lands. Whether you may trade on Polymarket at all depends on where you live. None of this is financial or legal advice.
What Python package should a Polymarket bot use?
Install polymarket-client, which was at 0.5.0 on 2026-08-09, and import it as polymarket, because the install and import names differ. It needs Python 3.11 or newer, and it ships often enough that you should pin an exact version and check PyPI rather than trusting a number in any tutorial. Ignore the archived py-clob-client, and do not start new projects on py-clob-client-v2, which the unified SDK supersedes.
Do you need an API key for Polymarket?
Not for reading. Market lists, prices, order books and price history all returned data with no credentials on 2026-08-07, and the market websocket channel is unauthenticated too. Credentials are needed only to place or manage orders, and they come from a wallet signature rather than a dashboard.
Conclusion
The plumbing is genuinely easy now, and that is the trap: free keyless data gets you a live price feed in ten minutes, which is easy to mistake for having a bot. The hard part was never the API, it was the rule in the middle and the discipline around it. Build the watcher first, run it in dry-run mode until its logged decisions earn trust, then add execution last, small, behind a flag you can flip off. The Polymarket API guide picks up where this stops, and we keep endpoint references across Predictefy checked against live responses.