Python SDK#
uselayer is one Python interface for trading prediction markets on Kalshi and Polymarket US with your own venue keys.
| Call | What it does | Where it runs |
|---|---|---|
client.matches(), client.match() |
Which Kalshi and Polymarket US markets are the same bet | Asks Layer |
client.prices() |
Both venues' best bids and asks, with sizes | Your machine, your keys |
client.buy(), client.trade() |
Orders on one venue, or both legs of a match | Your machine, your keys |
client.positions() |
What you hold; fills() and balances() too |
Your machine, your keys |
client.pnl() |
Profit and loss per market and in total | Your machine, your keys |
client.reconcile() |
Checks your local record against what each venue reports | Your machine, your keys |
Only the match lookup reaches Layer. It sends your Layer key, the market ids and your filters, and nothing else: your venue keys, prices and orders never leave your machine.
- Paper mode (the default) fills orders against the venues' real order books with simulated money. Nothing is sent to a venue.
- Live mode sends orders with your own Kalshi or Polymarket US key, for your own account.
- Backtest mode replays books you saved.
Every venue and mode takes the same order shape. Guardrails check every order before it's sent. Fees come from each venue's published schedule in force at the time of the trade, and match Layer's API (POST /v0/profit, POST /v0/size) to the millionth of a dollar. Everything stays on your machine: a local SQLite file per mode, no telemetry.
Install#
pip install uselayerPython 3.11 or newer. The package is on PyPI.
Paper trade#
from uselayer import Client
client = Client() # paper mode: real books, simulated fills
m = client.markets(limit=20)[0] # open Polymarket US markets, no key needed
book = client.book(m.slug)
order = client.order(
venue="polymarket_us", market=m.slug, side="yes", price=book.outcome("yes").best_ask.price, size=5
)
print(client.preview(order)) # fill, fees, every rule's decision
print(client.send(order)) # the order, filled against the bookpreview() shows what an order would do and sends nothing. client.positions(), client.fills() and client.orders() read the local store. Every paper fill is a SimulatedFill with simulated=True.
Live mode#
from uselayer import Client, PolymarketUS
client = Client(mode="live", polymarket_us=PolymarketUS(key_id="...", secret_key_path="~/.pmus/secret"))
client.balances()["polymarket_us"].cash
order = client.buy(venue="polymarket_us", market="<slug>", side="yes", price=0.42, size=5)
client.positions() # from the venueCreate the key at polymarket.us/developer. It stays on your machine: requests are signed with it locally and only the signature is sent.
- Books in live mode come from the venue's WebSocket, so they aren't cached.
- An order whose answer never arrives raises
outcome_unknown. It is never sent again on its own: callclient.sync()and checkclient.orders(). - If the local store is new but your account already has open orders or positions, live mode starts with the kill switch on, until you run
python -m uselayer resume --mode live.
Profit and loss#
p = client.pnl()
p.net, p.realized, p.unrealized, p.fees # dollars, in total
for r in p.rows: # one row per side of each market you've held
r.market, r.side, r.contracts, r.realized, r.unrealized, r.fees, r.outcomerealized: profit from contracts sold or settled, before fees.unrealized: the contracts you hold at the best bid, what you could sell them for now, minus what they cost.max_daily_lossvalues positions at the same bid.fees: every fee paid.net:realized + unrealized - fees.
A position with no bid to value it at shows unrealized=None, is listed in p.missing_marks and is left out of the total.
In paper and backtest mode, positions settle when their market does:
- Payout. Each contract pays $1 if its side won and $0 if it lost. On a
void, it pays the venue's price when the venue gives one (Kalshi's fair price for a canceled game), or else what the contract cost. Fees aren't refunded. - What happens next. The position closes, and resting orders on the market are canceled. Payouts are in
client.settlements(). - Paper mode asks the venue whether a market you hold has settled, at most once a minute per market, whenever you call
positions(),pnl()ormonitor().client.settle()asks now. - A backtest settles at each
resolutionevent it replays, and the market takes no more orders.
In live mode, pnl() reports what each venue says about your positions, including closed and settled ones, and values the contracts you hold at the bid. fees is what each venue charged: Kalshi reports it on each position. For Polymarket US it comes from your account's trade history, and so do positions Polymarket US has settled, which drop off its positions list.
Reconcile with the venues#
In live mode, the SDK keeps its own record of your orders and fills on your machine, and the guardrails count your positions from it. client.reconcile() reads each venue's fills, positions and open orders with your key and lists every difference:
r = client.reconcile()
r.ok # your record and every venue agree
for m in r.mismatches:
print(m.kind, m.venue, m.market, m.message)missed_fill: the venue filled an order the SDK sent, and your record doesn't have that fill (say, the process stopped mid-order).unknown_fill: your record has a fill for an SDK order that the venue doesn't show.outside_fill: a fill of an order the SDK didn't send, such as a trade on the venue's website or from another bot.position: the contracts you hold in a market, per your record, differ from the venue's. Holding NO shows as a negative number of YES contracts.outside_order: an open order on the venue that the SDK didn't send.stale_order: your record says an order is open, and the venue doesn't list it as open.
It only reads: your record doesn't change. client.reconcile(repair=True) also adds the fills the venue reported for orders the SDK sent. Fills of orders it didn't send are never added, only reported. Markets the venue has settled aren't compared. From a terminal, python -m uselayer reconcile prints the same list and exits 1 when anything differs, so you can run it on a schedule.
Guardrails#
client = Client(
rules={
"max_position": {"per_market": 200}, # $ at risk in one market
"budget": 1000, # $ at risk in total
"max_daily_loss": {"amount": 150}, # stop opening positions after this loss today
"approve_above": 100, # ask before orders above $100
"stop_loss": {"pct": 25}, # exits sent by client.monitor()
}
)Rules can also come from a YAML or JSON file: Client(rules="guardrails.yaml"). YAML needs pip install "uselayer[yaml]". Rules are fixed when the client is created.
Rules cover position size, budget, daily loss, allowed markets, approvals, stop-loss and take-profit. A price collar, an order throttle and a kill switch are always on.
Kill switch#
python -m uselayer kill # from any terminal on this machine
python -m uselayer status
python -m uselayer resume # a person turns it offclient.kill() does the same from code: it cancels resting orders and blocks new ones. The switch stays on, even after a restart, until a person runs python -m uselayer resume. The client a strategy or agent holds can't resume.
Pairs#
When two markets are the same bet, buying YES on one and NO on the other pays $1 per contract either way. quote() prices that after both fees. trade() places both legs.
q = client.quote(pair) # pair: a Match from client.matches(), or two (venue, market)
t = client.trade(pair, size=100, min_edge=0.01)
t.status # "hedged" | "missed" | "unwound" | "exposed"client.matches(q="...") needs a Layer API key. It sends Layer your key, the market ids Layer gave you and your filters, and nothing else: no prices, orders, positions or venue keys.
The leg-risk guard#
- The thinner leg goes first, immediate-or-cancel.
- The other leg goes for what filled, up to its break-even price.
- If it can't be completed within
chase_s, the first leg is sold back, never below its entry price minusmax_unwind_loss(on_miss="unwind", the default). Withon_miss="hold", the open contracts are reported instead. - Both legs pass the guardrails together before either is sent.
trade() runs in paper, backtest and live mode. Live, each leg goes to its venue with your own key. A Kalshi ↔ Polymarket US pair needs both keys, and both are checked before anything is sent:
client = Client(mode="live", kalshi=Kalshi.from_env(), polymarket_us=PolymarketUS.from_env())
t = client.trade(pair, size=10, min_edge=0.01)
if t.status == "exposed":
print(t.exposure, t.notes) # contracts left on one sideIf a venue answers with an error mid-pair, the open leg is still sold back or reported. If a venue can't say whether an order went through, nothing is sold back: the trade comes back exposed, and its notes say to run client.sync() before trading those markets again.
Prices, fees and profit#
prices() reads both sides of a match from each venue's order book, with your own keys. An empty side is None.
pair = client.matches(venue="polymarket_us", q="nfl")[0]
p = client.prices(pair) # or [("kalshi", ticker), ("polymarket_us", slug)]
k, u = p.leg("kalshi"), p.leg("polymarket_us")
k.yes_ask, k.yes_ask_size, u.no_ask, u.no_ask_size, k.as_offees() reads each market's fee settings, and how long your money would be held, from the venues (Kalshi ↔ Polymarket US pairs, paper and live mode):
client.fees(pair)
# {"kalshi": {"fee_type": "...", "fee_multiplier": 1.0},
# "polymarket_us": {"fee_coefficient": 0.0695}, "days_held": 6.76, "match": {...}}profit() takes the same body as POST /v0/profit and gives the same answer, on your machine. With pair=, it fills in each market's fees and days_held from the venues; anything you send wins. With a kalshi.market_id in the body instead, it asks Layer for the Polymarket US twin, its only call to Layer.
if k.yes_ask and u.no_ask: # an empty side is None, and profit() needs both prices
body = {"contracts": 100, "kalshi": {"price": k.yes_ask}, "polymarket_us": {"price": u.no_ask}}
r = client.profit(body, pair=pair)
r["net_profit"], r["filled_in"], r.get("return_per_day_pct") # only when days_held is knownuselayer.calc.profit() and uselayer.calc.size() are the same math with nothing read: you send every number, and calc.size() answers like POST /v0/size from the books you pass.
One strategy, every mode#
run() calls your strategy for each pair on each new book. The same function runs in every mode.
def strategy(client, pair, quote):
if quote.net_profit_per_contract >= 0.02:
client.trade(pair, size=100)
Client(mode="backtest", books=saved_books).run(strategy, [pair]) # the past
Client().run(strategy, [pair], iterations=60) # now, paperIn paper and live mode it runs every interval_s (1 second by default) until iterations rounds, stop() or the kill switch.
Backtest#
from uselayer import Client
from uselayer.backtest import load_books, record_books
record_books(Client(), ["<slug>"], "books.jsonl") # run on a schedule to build a history
bt = Client(mode="backtest", books=load_books("books.jsonl"))
bt.replay(lambda client, book: ...) # place orders as each book arrivesrecord_books() saves one snapshot per call. The replay uses the same fill model, fees and rules as paper mode, on the replayed clock. Trades in the data fill resting orders once the estimated line ahead of them at their price is used up.
To backtest on history you already have (CSV, Parquet, raw venue stream messages or PMXT files), see Backtest data.
Fees by date#
from datetime import UTC, datetime
from uselayer import FeeSettings, calculate_fee, rules_at
rules_at("polymarket_us", datetime.now(UTC)).source # the schedule's page
calculate_fee(
FeeSettings(venue="polymarket_us"), contracts=100, price=0.5, role="taker", at=datetime.now(UTC)
)Before the earliest schedule the SDK knows, it raises no_venue_rules instead of guessing.
What paper mode can't tell you#
- Your exact place in line. A resting paper order joins the back of the line at its price: everything already there is ahead of it. Trades at its price use up that line first, and only what's left fills your order. A trade through its price, or a book whose other side reaches it, fills it too, and the same contracts never fill it twice. Venues publish the total at each price, not single orders, so the line is an estimate: when a level shrinks by more than its trades explain, the difference counts as cancels, spread through the line (
Client(queue_cancels="behind")puts them all behind you, the worst case). With books alone (monitor()in paper mode, orrecord_books()snapshots), a resting order fills only when a book crosses its price. - How fast the venue answers. A paper order reaches the book the moment you send it.
- When a market settles. Paper mode learns that a market settled when it next asks the venue (at most once a minute per market). On Kalshi, a result counts only once it's final, not while it can still be disputed.
- Freshness without a key. In paper mode, Polymarket US's public book is cached for up to 30 seconds. The SDK stamps each book with the venue's time. When a copy is older than
max_quote_age_s(10 seconds by default), it waits for a fresh one before using it.
For AI agents#
AGENTS.mdandllms.txtship inside the package.- Every public method has a docstring with an example.
- Every object has
.to_dict(). - Every error is a
VenueErrorwithcode,hintandnext.
from uselayer import VenueError
try:
client.send(order)
except VenueError as e:
print(e.code, e.hint, e.next)Kalshi#
Kalshi's books are read with your own Kalshi API key, on your machine. Paper mode fills Kalshi orders against them with Kalshi's fee schedule and each series' fee multiplier.
from uselayer import Client, Kalshi
client = Client(kalshi=Kalshi(key_id="...", private_key_path="~/.kalshi/key.pem")) # or KALSHI_KEY_ID + KALSHI_PRIVATE_KEY_PATH
client.book("<TICKER>", venue="kalshi").outcome("yes").best_ask
client.buy(venue="kalshi", market="<TICKER>", side="yes", price=0.42, size=5) # paper fillCreate the key in your Kalshi account settings; Ed25519 and RSA keys both work. Market ids are Kalshi tickers. Kalshi(..., environment="demo") uses Kalshi's demo exchange. Backtest mode replays Kalshi books you saved without a key.
Live Kalshi orders. Client(mode="live", kalshi=...) places, cancels and tracks Kalshi orders with your own key, for your own account, as it does on Polymarket US. Your guardrails and the kill switch check every order first. Pairs across the two venues (trade()) run live too, with both keys.
trade() also runs on Kalshi ↔ Polymarket US pairs, in every mode:
client = Client(kalshi=Kalshi.from_env(), layer_key="lyr_...")
pair = client.matches(venue="polymarket_us", q="nfl")[0] # m.kalshi and m.polymarket_us
client.quote(pair).net_profit_per_contractOne more thing paper mode can't tell you: Kalshi's sub-cent billing. The SDK rounds each Kalshi fee up to the cent, from the published schedule, as Layer's API does. Kalshi bills fees to fractions of a cent, so a paper fee can be up to a cent higher than what Kalshi would charge.
Record every tick#
record_stream() keeps each venue's live stream open and saves every book change and trade, for Kalshi and Polymarket US in one call, one file and one event format, with the venue's time (as_of) and your machine's (received_at). Kalshi tickers (in capitals) and Polymarket US slugs can be mixed; venue= reads every id as one venue's. It reconnects on its own and writes a gap event for each market while its venue was disconnected. Kalshi numbers its messages, so a lost one is a gap too, followed by a fresh book. In backtest mode a market has no book during a gap. Each venue's stream needs your key for it, though nothing is traded: KALSHI_KEY_ID and KALSHI_PRIVATE_KEY_PATH (a read-only Kalshi key is enough), and POLYMARKET_US_KEY_ID and POLYMARKET_US_SECRET_KEY.
from uselayer import Client
from uselayer.backtest import load_books, record_stream
record_stream(["<TICKER>", "<slug>"], "ticks.jsonl", duration_s=3600) # Kalshi + Polymarket US, or until Ctrl-C
Client(mode="backtest", books=load_books("ticks.jsonl")).replay(on_book)In paper mode, pass on_event=client.feed so the stream's trades fill your resting orders as they print: record_stream(["<slug>"], "ticks.jsonl", on_event=client.feed).
From a terminal, with progress:
python -m uselayer record <TICKER> <slug> --out ticks.jsonl --minutes 60Import your own data#
import_events() turns history you already have (CSV, Parquet, raw venue stream messages or PMXT files) into backtest events, and checks it first. Backtests also replay polymarket.com data. See Backtest data.