Skip to main content

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, or record_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 after buy() 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)

Source distribution for uselayer 0.3.0
File Size Uploaded
uselayer-0.3.0.tar.gz 235.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for uselayer 0.3.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page