uselayer
A Python SDK for trading prediction markets with your own venue keys.
This release trades Polymarket US and Kalshi. Paper mode (the default) fills orders against the venue's real order books with simulated money and sends nothing to the venue. Live mode sends orders with your own Polymarket US or Kalshi API key, or both. Backtest mode replays books you saved or data you import.
- One order shape for every venue and mode, published as a JSON Schema (
schema/order.json). - Paper mode is the default.
preview()shows what an order would do and sends nothing. - Guardrails check every order before it's sent: 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.
- Fees come from each venue's published schedule in force at the time of the trade. They 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 uselayer
Python 3.11 or newer.
Paper trade in five lines
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 book
client.positions(), client.fills() and client.orders() read the local store. Every fill in paper
mode 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 venue
Kalshi works the same way with your Kalshi key (see Kalshi); pass both keys to trade both.
Create the Polymarket US 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 and is never sent again
on its own: call client.sync() and check client.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.
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; live mode sends them to Kalshi.
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 fill
Create 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 go to Kalshi with your own key, for your own account. Every guardrail applies to them as to any other order, and nothing about them goes to Layer:
client = Client(mode="live", kalshi=Kalshi.from_env()) # a Polymarket US key isn't needed
client.balances()["kalshi"].cash
o = client.buy(venue="kalshi", market="<TICKER>", side="yes", price=0.42, size=5)
client.cancel(o), client.positions(), client.fills()
client.kill() # cancels every resting Kalshi order too
Try it first on Kalshi's demo exchange (mock money) with a demo key and
Kalshi(..., environment="demo") (or KALSHI_ENV=demo); scripts/prove_kalshi_live.py runs the whole
lifecycle there. Kalshi's positions can trail a fill by a moment; positions() waits for them.
Live pairs across Kalshi and Polymarket US (trade()) need both keys: see Pairs.
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.outcome
realized is the profit from contracts sold or settled, before fees. unrealized values the
contracts you hold at the best bid, what you could sell them for now, minus what they cost.
fees counts every fee paid. net is 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.
max_daily_loss values positions at the same bid.
In paper and backtest mode, positions settle when their market does. 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.
The position closes, and resting orders on the market are canceled. Paper mode asks the venue
whether a market you hold has settled, at most once a minute per market, whenever you call
positions(), pnl() or monitor(). client.settle() asks now. A backtest settles at each
resolution event it replays, and the market takes no more orders. Payouts are in
client.settlements().
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
r = client.reconcile() # live mode
r.ok # the store and every venue agree
for m in r.mismatches:
m.kind, m.venue, m.market, m.message
In live mode the SDK keeps its own record of your orders and fills in the local store, and the
guardrails count your positions from it. reconcile() reads each venue's fills, positions and open
orders with your key and lists every difference:
missed_fill: the venue filled an order the SDK sent, and the store doesn't have that fill (say, the process stopped mid-order).unknown_fill: the store 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 the store, differ from the venue's. Positions are compared as net YES contracts, so holding NO shows as a negative number.outside_order: an open order on the venue that the SDK didn't send.stale_order: the store thinks an order is open, and the venue doesn't list it as open.
It only reads. Nothing in the store changes, and any mismatch also goes to on_alert.
client.reconcile(repair=True) runs sync(), then adds the fills the venue reported for orders the
SDK sent; fills of orders it didn't send stay reported, never added. since= sets where the fill
comparison starts (default: just before the store's first order). Markets the venue has settled
aren't compared. Positions are compared for the whole account, so positions from orders sent with
another store show as position mismatches. Polymarket US can take a moment to list a new trade:
a reconcile() run right after a fill can report it as unknown_fill (seen live: a trade 0.2 s
old wasn't listed yet), and running it again a little later clears it. From a terminal, python -m uselayer reconcile prints the same list and exits 1
when anything differs, so it can run 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]"). They're fixed when the client is created.
Kill switch. client.kill() cancels resting orders and blocks new ones. From another terminal:
python -m uselayer kill. It 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: both sides, with the leg-risk guard
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"
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 minus max_unwind_loss (on_miss="unwind", the default), or the open contracts are
reported (on_miss="hold"). Both legs pass the guardrails together before either is sent.
trade() runs in paper, backtest and live mode, including Kalshi ↔ Polymarket US pairs:
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_contract
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 side
If 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.
One strategy, 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, paper
Backtest on books you saved
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 arrives
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.
Record every tick
record_books() saves one snapshot per call. record_stream() keeps the venues' live streams open
and saves every book change and trade, with the venue's time (as_of) and your machine's
(received_at). One list can mix Polymarket US slugs and Kalshi tickers; each venue connects with
your own key for it (for Kalshi, a read-only key is enough), though nothing is traded. It reconnects
on its own and writes a gap event for each market while it 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.
from uselayer.backtest import load_books, record_stream
record_stream(["<slug>", "<KALSHI-TICKER>"], "ticks.jsonl", duration_s=3600) # 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 <slug> <KALSHI-TICKER> --out ticks.jsonl --minutes 60.
Backtest on data you already have
import_events() turns history you already keep into the SDK's events, checks it, and hands it to a
backtest:
from uselayer import Client, import_events
data = import_events(
"ticks.csv",
venue="kalshi",
columns={"time": "ts", "market": "ticker", "bid": "yes_bid", "bid_size": "yes_bid_qty",
"ask": "yes_ask", "ask_size": "yes_ask_qty"},
price_scale=0.01, # the file has cents
)
print(data.report.summary())
Client(mode="backtest", books=data).replay(on_book)
It reads:
format= |
What |
|---|---|
csv, parquet |
One row per event; columns= maps the SDK's field names to yours (Parquet needs pip install 'uselayer[parquet]') |
jsonl |
The SDK's own format, as save_events() writes it |
polymarket_us, polymarket, kalshi |
Raw messages from each venue's market-data WebSocket, one per line, optionally as {"received_at": ..., "message": ...} |
pmxt |
PMXT's hourly Polymarket order-book Parquet files (both schemas); pass markets= |
The format is guessed from the file when you leave it out. Pass a list of files to join consecutive hours.
Every import is checked before it can reach a replay. Crossed books, impossible values, prices off
the tick size, unreadable rows and lost Kalshi messages raise VenueError("bad_data") with a report;
gaps, rows out of time order and re-sent old books are warnings in data.report. Pass strict=False
to import a file with errors anyway. The event format and every field are documented at
uselayer.sh/docs/backtest-data.
examples/08_import_and_backtest_a_pair.py does it end to end for a Kalshi ↔ Polymarket US pair:
it imports a file from each venue, checks them together, and backtests trade() and pnl() on them.
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. By default a paper order reaches the book the moment you send it.
Client(order_latency_s=0.7)makes it arrive that many seconds later: it fills against the book it meets then, and trades before then can't fill it. On Polymarket US (2026-10-04, 8 orders) an order landed in the book 0.6–1.4 s afterbuy()was called, median about 0.7 s. - 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 and, when a copy is older than
max_quote_age_s(10 s by default), waits for a fresh one before using 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). Kalshi is asked only once a result is final, not while it can still be disputed.
- 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.
For AI agents
AGENTS.md and llms.txt ship inside the package. Every public method has a docstring with an
example, every object has .to_dict(), and every error is a VenueError with code, hint and
next. The examples/ folder runs in CI.
Layer
Layer (uselayer.sh) finds markets that are the same bet on different venues. With a Layer API key,
client.matches(q="...") returns them. The SDK sends Layer your key, the market ids Layer gave you
and your filters, and nothing else: no prices, orders, positions or venue keys.
Layer answers with each market's ids and url, and its own match confidence and rule-difference flags
(m.confidence, m.caveats). The SDK then reads each market's event, question, outcome and times
from its venue's public API, on your machine, so m.kalshi.outcome and m.polymarket_us.question
are filled in. client.matches(..., titles=False) skips those venue calls.
License
MIT
Metadata
Release files for uselayer 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| uselayer-0.3.0.tar.gz | 235.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| uselayer-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 402.1 kB
Release files / uselayer-0.3.0.tar.gz
| Download URL | uselayer-0.3.0.tar.gz |
|---|---|
| Size | 235.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0ab1200cc368be77fb68eecff34b00813b7445de4956fcf64ab592fc0cc04dda
|
|
BLAKE2b-256 checksum How to use checksums |
75552193a56981f854780cb23255c415037f9f15aa1914fae6d4f947f60155c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / uselayer-0.3.0-py3-none-any.whl
| Download URL | uselayer-0.3.0-py3-none-any.whl |
|---|---|
| Size | 167.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0a74995dbe97c4c42549698587cd8824a663de05858e5e8ab64182882d2433f3
|
|
BLAKE2b-256 checksum How to use checksums |
88a14a6e3e38522e7032ee78fed5362cfb90637a99833dddbaac48dc281039ac
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|