If it is your first bot of any kind, Python is the friendliest path onto Polymarket. The language is forgiving, the libraries that matter are small in number, and the official client does most of the cryptography for you. This tutorial is written for a reader who can write a for-loop and use pip, but has never signed an Ethereum transaction or talked to a CLOB. The plan is to walk from an empty folder to a script that fetches markets, signs an order, places a tiny safe trade, and polls for fills, while explaining what every step is doing and where it is safe to stop. The aim is not to ship a profitable strategy in one sitting; the aim is to get the auth, signing, and order-placement plumbing working so that strategy code becomes a small layer on top.
Why Python is the easiest first language for a Polymarket bot
There is no shortage of online debate about which language is best for crypto trading. Most of it is irrelevant to a first project. The honest answer is that the binding constraint on a beginner bot is not language speed; it is the time it takes to understand wallets, signing, fees, gas, and the difference between a market-buy and a limit-buy. Anything that gets a working order on a real market in fewer hours is the right tool, and for Python that path is shorter than for any other language because Polymarket maintains an official Python client.
Concretely, three things make Python the soft landing. First, the py-clob-client library covers the cryptography that would otherwise take a week to learn: EIP-712 typed-data signing, API-key derivation, order construction, and the REST endpoints all sit behind small synchronous functions. Second, the rest of the ecosystem the bot needs is shallow: requests for plain HTTP, web3.py for any direct chain calls, python-dotenv for secrets, asyncio when polling becomes a loop. Third, the iteration loop is fast; a small change to a Python file is testable in seconds, which matters more than execution speed when the operator is still figuring out what the bot should even do.
There is a real ceiling. If a future strategy needs to react to book updates in under 200 milliseconds, Python will be the constraint and Rust or Go become the right choice. That moment is months away for a beginner. For the build described in this tutorial, the gap between Python and a hand-tuned Rust client is measured in tens of milliseconds against a target window of seconds. The simpler the first build, the easier it is to outgrow it later.
The libraries you actually need
The Python crypto-tooling ecosystem is large and most of it is not useful for this project. Four libraries cover the entire surface area of a beginner Polymarket bot. Anything else is either premature or already wrapped by one of these four.
| Library | What it does | Order signing | WebSocket | When to use |
|---|---|---|---|---|
py-clob-client (official) | High-level Polymarket CLOB client | Built in (EIP-712) | No (REST only) | Default. Use unless there is a reason not to. |
Custom requests + manual signing | Direct REST calls, hand-rolled signing | You implement it | No | Only when the official client lacks an endpoint or you need fine control. |
web3.py | Polygon RPC, wallet, on-chain reads | Generic EIP-712, not Polymarket-specific | Yes (via provider) | Approvals, balance checks, gas estimation, direct chain reads. |
websockets / websocket-client | Real-time order book updates | No | Yes | Only after the REST polling loop is working and stable. |
The honest recommendation for a first build is: start with py-clob-client and web3.py, ignore everything else. Add websockets only when a polling loop is no longer fast enough. Do not roll your own signing layer until you have read the official client's signing code end to end. The signing rules are subtle and a single byte-order mistake produces an order that the gateway silently rejects without an obvious error.
The official client lives at github.com/Polymarket/py-clob-client. Read the readme before installing. Skim the source for the ClobClient.create_order and ClobClient.post_order functions; the names and arguments here will recur throughout the tutorial.
Setting up the project
Project layout matters more than it looks. A clean structure now saves an hour of debugging later when a config value is suddenly in the wrong place. The recommended layout is small but opinionated.
polymarket-bot/
.env # secrets, never committed
.env.example # template, committed
.gitignore # at minimum: .env, __pycache__, .venv
requirements.txt
src/
__init__.py
config.py # loads .env into a typed Config object
client.py # py-clob-client wrapper
markets.py # market discovery and filtering
orders.py # order building and submission
loop.py # the polling loop
safe.py # size caps, kill switch, sanity checks
scripts/
fetch_markets.py # one-shot: list active markets
place_test_order.py # one-shot: a $1 test order
run_bot.py # the long-running loop
tests/
test_safe.py
Create the virtual environment with the standard venv module rather than conda or poetry. The fewer moving parts, the fewer ways a tutorial step can fail on a different machine.
# bash on macOS or Linux; on Windows use the equivalent in PowerShell
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install py-clob-client web3 python-dotenv requests
pip freeze > requirements.txt
Now the .env file. This holds three secrets and never leaves the machine. The shape is fixed by what the official client expects; the values come from a wallet you control. If you do not yet have a wallet, generate a fresh one with MetaMask, fund it with a small amount of MATIC for gas and USDC for trading, and copy the private key. Do not reuse a wallet that holds significant assets; a bot wallet is a hot wallet by definition and should be sized for the experiment, not your life savings.
# .env — never commit this file
POLY_PRIVATE_KEY=0xabc123... # the L1 wallet private key (Polygon EOA)
POLY_FUNDER_ADDRESS=0xYourPolymarketProxy # the proxy address Polymarket assigns
POLY_API_KEY= # filled in after key derivation
POLY_API_SECRET= # filled in after key derivation
POLY_API_PASSPHRASE= # filled in after key derivation
POLY_HOST=https://clob.polymarket.com
POLY_CHAIN_ID=137
MAX_ORDER_USD=2.0 # hard cap on size, dollars
DRY_RUN=true # do not actually submit while learning
The DRY_RUN flag is the most important line in the file. Every code path that submits an order should check it and print the payload instead of sending it whenever it is true. Flip it to false only when the script has been read end to end.
Authenticating: the L1 wallet, L2 API keys, and the signing dance
Polymarket's authentication model has two layers, and confusing them is the most common stumbling block for a first build. The L1 layer is the Ethereum-style wallet, identified by a private key and an address; it signs orders with EIP-712 typed data. The L2 layer is an API key, secret, and passphrase, derived from the L1 wallet by a one-time call to the gateway; it authenticates REST requests so the server knows which orders belong to which account. The L1 wallet is sovereign and stays on your machine. The L2 keys are gateway-issued and can be rotated.
The one-time derivation looks like this. Run it once; copy the three values into .env; never run it again unless you want to rotate.
# scripts/derive_api_keys.py
import os
from dotenv import load_dotenv
from py_clob_client.client import ClobClient
load_dotenv()
client = ClobClient(
host=os.environ["POLY_HOST"],
key=os.environ["POLY_PRIVATE_KEY"],
chain_id=int(os.environ["POLY_CHAIN_ID"]),
)
creds = client.create_or_derive_api_creds()
print("POLY_API_KEY=", creds.api_key)
print("POLY_API_SECRET=", creds.api_secret)
print("POLY_API_PASSPHRASE=", creds.api_passphrase)
The signing dance happens automatically once those values are loaded. Every time the bot builds an order, the client serialises it according to the EIP-712 typed-data schema for Polymarket's CTF Exchange, hashes it, signs the hash with the L1 private key, and attaches the signature to the REST payload along with the L2 API key headers. From the bot author's point of view this is one function call. From a learning point of view it is worth opening the client's source and reading the build_order and sign_order implementations once, so the abstraction is not a black box.
Two practical safety rules. First, the private key never leaves the bot machine and never appears in a log line. Treat any code that touches it as security-critical. Second, the L2 keys give read and trade access but not withdraw access; if they leak the worst case is unauthorised orders, not a drained wallet. Rotate them on suspicion; the L1 key only on confirmed compromise (which means moving funds to a fresh wallet).
Your first GET: fetching market data
Before any order, the bot needs to know what markets exist. The official client exposes paginated reads of active markets, each with an ID, a question, the outcome tokens, the current order book, and resolution metadata. The first script the operator writes is a one-shot that prints a handful of markets and exits. Nothing trades, nothing signs; the goal is to see live data flow into the process.
# scripts/fetch_markets.py — first read, no orders
import os
from dotenv import load_dotenv
from py_clob_client.client import ClobClient
load_dotenv()
client = ClobClient(
host=os.environ["POLY_HOST"],
key=os.environ["POLY_PRIVATE_KEY"],
chain_id=int(os.environ["POLY_CHAIN_ID"]),
creds=None,
)
# Read the first page of active markets
page = client.get_markets(next_cursor="")
markets = page.get("data", [])
print(f"Fetched {len(markets)} markets on this page.")
for m in markets[:5]:
q = m.get("question", "?")
cid = m.get("condition_id", "?")
end = m.get("end_date_iso", "?")
print(f"- {q[:70]:70s} ends={end} id={cid[:10]}...")
What to look for in the output. The question field is human-readable and is the field most beginner-friendly filters use. The condition_id is the on-chain identifier; it is the value that downstream order calls take, not the question text. The end_date_iso shows resolution time; markets with an end date inside the next hour or two are usually too thin or too noisy to be a first-trade target. Skim ten or twenty markets and pick one that resolves in the next several days with a clear question; that is the candidate for the first test order.
If this script prints an authentication error, the most common cause is a private key in the wrong format (it must include the 0x prefix) or the wrong chain id (Polygon mainnet is 137). If it prints zero markets, the host URL is wrong; the default https://clob.polymarket.com is correct unless Polymarket announces otherwise.
Your first POST: placing a tiny safe order
This is the step where most beginners stall, not because the code is hard, but because there is a real understandable reluctance to push the button on a live trade. The way to defuse that is to use a size so small that a total loss is uninteresting, and to keep DRY_RUN on for the first several runs. Two dollars is a reasonable starting size; the minimum order on Polymarket is small enough that this is well above the floor.
The order itself is a limit order at a price you are happy to pay, sized to roughly two dollars at that price. Limit orders are safer than market orders for a beginner because the worst-case price is bounded; a market order can fill against a thin book at a price far worse than expected.
# scripts/place_test_order.py — one tiny limit order, dry-run by default
import os
from dotenv import load_dotenv
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import OrderArgs, OrderType
from py_clob_client.order_builder.constants import BUY
load_dotenv()
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
MAX_USD = float(os.environ.get("MAX_ORDER_USD", "2.0"))
client = ClobClient(
host=os.environ["POLY_HOST"],
key=os.environ["POLY_PRIVATE_KEY"],
chain_id=int(os.environ["POLY_CHAIN_ID"]),
creds=None,
funder=os.environ.get("POLY_FUNDER_ADDRESS") or None,
)
client.set_api_creds(client.create_or_derive_api_creds())
# Pick a token id from a market you actually read above
TOKEN_ID = "0xabc...replace-with-a-real-yes-or-no-token-id"
PRICE = 0.20 # 20 cents per share, well below typical mid
SIZE = round(MAX_USD / PRICE, 2) # dollars / price = share count
args = OrderArgs(
token_id=TOKEN_ID,
price=PRICE,
size=SIZE,
side=BUY,
)
if DRY_RUN:
signed = client.create_order(args)
print("DRY_RUN: would submit:", signed)
else:
signed = client.create_order(args)
resp = client.post_order(signed, OrderType.GTC)
print("submitted:", resp)
A few points on what the code is doing. The OrderArgs fields are exactly what they look like: a token id, a price between 0 and 1, a size in shares, and a side (buy or sell). The OrderType.GTC argument is good-til-cancelled, which is the right default for a limit order that should sit on the book. For a market-style fill, use OrderType.FOK (fill-or-kill), but only after the order book is well understood; a fill-or-kill at the wrong price simply does nothing and that silent no-op is confusing for a beginner.
The first run should be DRY_RUN=true and the output is the signed order payload as a Python dict. Read it. Notice the signature field; that is the EIP-712 signature, and it is the proof to the gateway that the order came from your wallet. Once the payload looks sensible, flip DRY_RUN=false for one run, watch the response, then immediately flip it back. A pattern of leaving dry-run on as the default and explicitly turning it off only for intentional submissions is the simplest safety habit.
The polling loop and lifecycle
A bot is a loop. Each iteration does the same small set of steps: load config, fetch the markets it cares about, score them against the strategy, decide whether to act, sign and submit any orders, track what happens, log everything for later inspection. The figure below shows the lifecycle; the code that follows is a minimal version of the same flow.
Polymarket beginner bot — polling loop lifecycle
The polling loop in code is short. The interesting decisions are not in the loop scaffolding itself but in the score and decide functions, which are deliberately left as stubs to be filled with whatever strategy the operator is exploring. For a first run, the strategy can be as simple as “print the five lowest-priced YES tokens on markets resolving in the next 48 hours and do nothing else.”
# scripts/run_bot.py — minimal polling loop
import os, time, traceback
from dotenv import load_dotenv
from py_clob_client.client import ClobClient
load_dotenv()
SLEEP_SECONDS = 15
def load_client():
c = ClobClient(
host=os.environ["POLY_HOST"],
key=os.environ["POLY_PRIVATE_KEY"],
chain_id=int(os.environ["POLY_CHAIN_ID"]),
creds=None,
)
c.set_api_creds(c.create_or_derive_api_creds())
return c
def fetch(client):
return client.get_markets(next_cursor="").get("data", [])
def score(markets):
# placeholder: return whatever subset your strategy likes
return [m for m in markets if m.get("active") and not m.get("closed")][:10]
def decide(candidates):
# placeholder: return zero orders while learning
return []
def submit(client, orders):
for o in orders:
signed = client.create_order(o)
print("DRY_RUN payload:", signed) # keep dry-run while learning
def log(iteration, candidates, orders):
print(f"[{iteration}] {len(candidates)} candidates, {len(orders)} orders")
def main():
client = load_client()
i = 0
while True:
i += 1
try:
markets = fetch(client)
candidates = score(markets)
orders = decide(candidates)
submit(client, orders)
log(i, candidates, orders)
except Exception:
traceback.print_exc()
time.sleep(SLEEP_SECONDS)
if __name__ == "__main__":
main()
A few habits worth forming early. The loop catches every exception and continues; a network blip should not kill the bot. Every iteration prints a one-line summary so a glance at the terminal shows whether the bot is alive and roughly what it is doing. The sleep is generous; 15 seconds is plenty when the strategy is still in the “print things” phase, and faster cadences are easy to add later if and when they are needed. Order submission stays dry-run inside submit until the operator removes that line by hand.
Safe defaults that protect a first-time bot
The largest source of beginner losses on Polymarket is not strategy error; it is a small bug or a forgotten flag that lets the bot do something the operator never intended. The mitigations are mechanical and worth coding in from day one rather than promising to add later.
A hard size cap, in dollars, in code. Every code path that builds an order should pass through a single check_size function that compares the dollar value of the order to MAX_ORDER_USD and refuses to sign anything larger. Even when the operator believes the strategy will only ever produce small orders, a bug elsewhere can produce a huge one; the check is the seatbelt. Start with two dollars while learning, raise it slowly only after each step proves itself.
A market allowlist or filter. A first-time bot should not trade every market the gateway returns. Filter by minimum 24-hour volume, by category, by resolution distance, or by an explicit list of condition IDs the operator has read by hand. The filter is a safety feature as much as a strategy feature; it bounds the universe of things the bot can do wrong.
A kill switch file. A one-line check at the top of every iteration: if a file called STOP exists in the working directory, exit cleanly. Creating that file is then the operator's panic button; no need to find the right terminal and hit ctrl-C. The pattern is trivial to add and has saved more than one bot from a runaway loop.
Logging that is greppable later. Every order built, every submission attempted, every fill or rejection, gets a single structured log line with a timestamp, the market id, the side, the size, and the outcome. When something surprising happens an hour later, the only way to reconstruct what the bot did is the log; if it is not written, the event is lost. Print to stdout is fine for a first build; rotate to a file once the bot runs continuously.
Separate read and trade phases. The first version of any new strategy should run with the submit step replaced by a print. Watch what it would have done for at least a day. Only then turn on real submissions, and only at the smallest size. The companion build in how to build a Polymarket trading bot walks through the same shape at a less beginner level; the arbitrage variant in the arbitrage bot tutorial shows what changes when latency starts to matter; and the engineering overview at Polymarket bot architecture sketches how these pieces compose into a longer-lived system.
None of these defaults makes the bot smarter. All of them make the bot survivable while the operator is still learning what smart means. The first months of running any new bot are an education paid for in small losses; the defaults keep those losses small enough to be tuition rather than a postmortem.
About the author
Jamal Okafor is an execution engineer on the Poly Syncer team. He spends most of his time on low-latency order routing and cross-venue plumbing, and the rest of it patiently walking newer builders through the first ten percent of any new bot project, because that is where the costly habits get set.
Frequently asked questions
Do I need to know Solidity to write a Polymarket bot in Python?
No. The official Python client handles all the contract calls and EIP-712 signing internally. A Polymarket bot author needs to understand what an order looks like as a Python dict and what a private key signs, but not the contract internals. Reading the CTF Exchange contract is a useful side project for builders who want to debug rejections, but it is not a prerequisite to a working bot.
How much money do I need to start a Polymarket Python bot?
Enough USDC for a few test trades plus a few dollars of MATIC for gas. Twenty to fifty dollars of USDC and five dollars of MATIC is plenty for the learning phase. Real strategy testing usually wants a few hundred dollars so that fills are not constrained by the size cap, but that is a later step. Never start with money you cannot lose entirely; bugs in the early weeks of a new bot are normal.
What is the difference between the L1 private key and the L2 API keys?
The L1 private key is your wallet private key on Polygon; it signs every order with EIP-712 typed data and can move funds. The L2 keys (API key, secret, passphrase) are derived from the L1 wallet by a one-time call and authenticate REST requests to the Polymarket gateway. The L1 key stays on your machine forever; the L2 keys can be rotated. Both must be loaded for the official client to place orders.
Can I run a Polymarket Python bot on my laptop?
Yes, for a beginner build. A laptop with a stable internet connection is fine while the strategy is being explored. Move to a small cloud instance (a five-dollar-a-month box is enough) once the bot needs to run continuously without the laptop being awake. There is no advantage to expensive infrastructure for a polling-style Python bot; the bottleneck is network round-trip to the gateway, not local compute.
What if the official py-clob-client does not expose the endpoint I need?
That is when you drop to requests and hand-roll a REST call against the documented endpoint, reusing the API key headers that the client built for you. Read the client source for examples of how the request signing and headers are constructed; the patterns are short and copyable. Avoid building a parallel implementation of the order-signing layer until you have read the client's signing code end to end; subtle errors in EIP-712 serialisation are the single largest cause of silently rejected orders.