Bayse Markets Python SDK
Python 3.12+ · MIT
Async Python SDK for the Bayse Markets prediction market API — fully typed, Pythonic, and covering every endpoint.
This is an independent, community-built SDK by Ogie Omorose — not built, maintained, or endorsed by Bayse.
from bayse_markets import BayseClient
async with BayseClient(public_key="pk_live_...", secret_key="sk_live_...") as client:
events = await client.list_events(page=1, size=10)
for event in events.data.events:
print(f"{event.title}: {event.closing_date}")
Who this is for
- App developers building trading bots, dashboards, or integrations on top of Bayse — full order lifecycle (place, batch, amend, cancel), portfolio, wallet, and activity coverage.
- Data scientists & quantitative researchers — every response is a Pydantic model that drops straight into
pandas(see Data Science Quickstart) for backtesting, price history analysis, or signal research. - Market makers / liquidity providers — dedicated
market_maker(mint/burn),maker_rebates, andrewardsmodules for tracking and optimizing liquidity incentives. - Anyone bootstrapping API access —
UserClientcreates and rotates API keys from just an email/password, no dashboard round-trip required.
Install
pip install bayse-markets
# or
uv add bayse-markets
Requires Python 3.12+.
See CHANGELOG.md for release notes. 0.3.0 adds buy() and
sell() helpers — see Buy & Sell below. 0.2.0 contains breaking
changes — if upgrading from 0.1.0, read
Migrating to 0.2.0.
Configuration
The client works with just two keys, but every knob is adjustable.
from bayse_markets import BayseClient
from bayse_markets._config import Env
async with BayseClient(
public_key="pk_live_...",
secret_key="sk_live_...",
env=Env.SANDBOX, # or "sandbox" — defaults to production
timeout=30.0,
) as client:
...
Retry and trace behaviour are passed as objects:
from bayse_markets._config import RetryConfig, TraceConfig
from bayse_markets._retry import RetryStrategy
client = BayseClient(
public_key="pk_live_...",
secret_key="sk_live_...",
retry_strategy=RetryStrategy(RetryConfig(max_retries=3, base_delay=0.5)),
trace_config=TraceConfig(session_id="my-bot"),
)
All configuration fields
BayseClient(...)
| Argument | Default | Meaning |
|---|---|---|
env |
Env.PRODUCTION |
PRODUCTION → relay.bayse.markets, SANDBOX → sandbox.relay.bayse.markets. Accepts the string "sandbox" too. |
timeout |
30.0 |
HTTP timeout in seconds. |
retry_strategy |
RetryStrategy(RetryConfig()) |
See below. |
trace_config |
TraceConfig() |
See below. |
signer |
HMAC-SHA256 | Override only if you need custom request signing. |
RetryConfig
| Field | Default | Meaning |
|---|---|---|
max_retries |
5 |
Attempts after the first. 0 disables retries. |
base_delay |
1.0 |
Base for the exponential backoff, in seconds. |
max_delay |
60.0 |
Ceiling on any single delay. |
jitter |
0.1 |
Random 0–jitter seconds added per delay. |
retry_on_statuses |
(429, 500, 502, 503, 504) |
Applied to safe and idempotent requests. |
retry_unsafe_on_statuses |
() |
Applied to non-idempotent requests. Empty by default — see Reliability & Retries. |
safe_methods |
{GET, HEAD, OPTIONS} |
Never has side effects. |
idempotent_methods |
{PUT, DELETE} |
Idempotent by HTTP specification. |
TraceConfig
| Field | Default | Meaning |
|---|---|---|
session_id |
random 12-char hex | Prefix for generated trace IDs. |
start_sequence |
1 |
First sequence number. |
Key Features
- 100% typed responses — every endpoint returns
BayseResponse[T]with proper Pydantic models. No raw dicts. - Pythonic field names — API
camelCasefields are mapped tosnake_case. Writeevent.closing_date, notevent["closingDate"]. - UserClient — bootstrap API keys programmatically from email + password. No need to visit the web UI.
- No auth needed for some endpoints — price history and order books are public.
- Full API coverage — events, orders (single + batch), quoting, portfolio, PnL, trades, activities, wallet, sports, liquidity rewards, maker rebates, market maker, system health.
- Async only — built on
httpxwith method-aware retries, exponential backoff, and trace IDs. A failedGETis replayed; a failed order is not. - Retry-safe writes — the batch endpoints support real, server-verified idempotency keys.
- AI-friendly reference —
llms.txtat the project root gives AI coding tools a condensed, complete reference for using this SDK.
Quick Start
Already have API keys
import asyncio
from bayse_markets import BayseClient
async def main():
async with BayseClient(
public_key="pk_live_...",
secret_key="sk_live_...",
) as client:
# Typed responses — use dot access, not dict keys
pnl = await client.get_pnl(time_period="1M")
print(f"PnL: {pnl.data.realized_pnl} ({pnl.data.realized_pnl_percent}%)")
portfolio = await client.get_portfolio()
for balance in portfolio.data.outcome_balances:
print(f"{balance.market.title}: {balance.outcome} = {balance.balance}")
events = await client.list_events(page=1, size=5)
for event in events.data.events:
print(f"{event.title} — closes {event.closing_date}")
asyncio.run(main())
Don't have keys yet — bootstrap with UserClient
from bayse_markets import UserClient, BayseClient
async with UserClient() as user:
await user.login("you@example.com", "your-password")
key = await user.create_api_key("my trading bot")
# Save these — secret_key is only shown once
print(f"pk={key.public_key}")
print(f"sk={key.secret_key}")
# Now use them with BayseClient
async with BayseClient(
public_key=key.public_key,
secret_key=key.secret_key,
) as client:
wallet = await client.get_assets()
for asset in wallet.data.assets:
print(f"Wallet balance: {asset.available_balance}")
portfolio = await client.get_portfolio()
print(f"Balance: {portfolio.data.portfolio_current_value}")
More Examples
Buy & Sell
Two high-level helpers for the most common operations. They call place_order
under the hood with the correct side and clearer parameter names.
# Buy: spend 10,000 NGN on an outcome
order = await client.buy(
event_id="evt_...",
market_id="mkt_...",
outcome_id="outcome_uuid",
amount=10000, # cash spend in the target currency
order_type="LIMIT",
price=0.65,
currency="NGN",
)
print(f"Buy order {order.data.order.id} — {order.data.order.status}")
# Sell: liquidate 5.84 shares of an outcome
# event_id / market_id resolved from portfolio automatically
order = await client.sell(
outcome_id="outcome_uuid",
shares=5.84, # share count to sell, NOT cash value
currency="NGN",
)
print(f"Sell order {order.data.order.id} — {order.data.order.status}")
Pass event_id and market_id explicitly to skip the portfolio lookup:
order = await client.sell(
event_id="evt_...",
market_id="mkt_...",
outcome_id="outcome_uuid",
shares=5.84,
currency="NGN",
)
place_order — low-level control
For advanced use cases that need every parameter (e.g. stp_mode,
max_slippage, post_only, time_in_force).
Important: amount has different semantics per side:
side="BUY":amountis the cash spend in the target currency.side="SELL":amountis the number of shares to liquidate.
For most use cases, prefer buy() or sell() — they set the correct side
and name the parameter appropriately.
order = await client.place_order(
event_id="evt_...",
market_id="mkt_...",
side="BUY",
outcome_id="outcome_uuid",
amount=10000,
order_type="LIMIT",
price=0.65,
currency="NGN",
)
print(f"Order {order.data.order.id} — {order.data.order.status}")
Batch orders
result = await client.batch_place_orders(body={
"orders": [
{
"marketId": "...", "side": "BUY", "outcomeId": "outcome_uuid_yes",
"amount": 5000, "type": "LIMIT", "price": 0.65, "currency": "USD",
},
{
"marketId": "...", "side": "SELL", "outcomeId": "outcome_uuid_no",
"amount": 3000, "type": "LIMIT", "price": 0.30, "currency": "USD",
},
]
})
print(f"{result.data.summary.succeeded} placed, {result.data.summary.failed} failed")
Each order is routed by
outcomeIdalone —marketIdis accepted but not validated, so a mismatchedmarketIdwon't misroute an order (it's derived from the outcome server-side). Don't rely on it as a safety check.
Get a quote
quote = await client.get_quote(
event_id="evt_...",
market_id="mkt_...",
side="BUY",
outcome_id="outcome_uuid",
amount=500,
currency="USD",
)
print(f"Price: {quote.data.price}")
Sports
from bayse_markets.sports import list_leagues, list_games
leagues = await list_leagues(client)
for league in leagues.data:
print(f"{league.name} ({league.sport})")
games = await list_games(client, league="EPL", page=1, size=10)
for game in games.data:
print(f"{game.home_team.name} vs {game.away_team.name} — {game.start_time}")
Liquidity rewards
from bayse_markets.rewards import list_rewards, get_active_rewards
payouts = await list_rewards(client, page=1, size=20)
for reward in payouts.data.data:
print(f"Epoch {reward.epoch_id}: {reward.payout} NGN — {'paid' if reward.is_paid else 'pending'}")
active = await get_active_rewards(client)
for epoch in active.data.data:
print(f"Active: {epoch.accumulated_shares} shares → ~{epoch.estimated_payout} NGN")
Maker rebates
from bayse_markets.maker_rebates import list_maker_rebates, get_active_maker_rebates
rebates = await list_maker_rebates(client, page=1, size=20)
for rebate in rebates.data.data:
print(f"Rebate: {rebate.rebate_amount} NGN — maker volume {rebate.maker_volume}")
Market maker (mint / burn)
from bayse_markets.market_maker import mint_shares, burn_shares
mint = await mint_shares(client, market_id="mkt_...", quantity=100, currency="USD")
print(f"Minted: {mint.data.shares_received} shares")
burn = await burn_shares(client, market_id="mkt_...", quantity=50, currency="USD")
print(f"Burned: {burn.data.cash_received} USD")
Order books
books = await client.get_order_books(
outcome_ids=["outcome_id_1", "outcome_id_2"],
depth=5,
)
for book in books.data:
print(f"Market {book.market_id}: {len(book.bids)} bids, {len(book.asks)} asks")
if book.last_traded_price:
print(f" Last trade: {book.last_traded_price} ({book.last_traded_side})")
Price history
history = await client.get_price_history(
event_id="evt_...",
time_period="1W",
outcome="YES",
)
for market_id, points in history.data.items():
print(f"Market {market_id}: {len(points)} data points")
for point in points[:3]:
print(f" {point.timestamp}: {point.price}")
Manage API keys (UserClient)
from bayse_markets import UserClient
async with UserClient() as user:
await user.login("you@example.com", "password")
keys = await user.list_api_keys()
for k in keys.keys:
print(f"{k.name}: {k.public_key} ({k.secret_key_hint})")
key = await user.create_api_key("new key")
print(f"Created: {key.public_key} / {key.secret_key}")
new_key = await user.rotate_api_key(key.id)
print(f"Rotated: new secret = {new_key.secret_key}")
await user.revoke_api_key(key.id)
Lookup a user
user = UserClient()
user.public_key = "pk_live_..."
profile = await user.lookup_user(tag="mulumba")
print(f"{profile.tag} — {profile.image_url}")
Utilities
Convenience functions that combine or derive data from multiple API calls.
Import from bayse_markets.utils:
from bayse_markets.utils import (
get_closing_soon,
get_high_volume_markets,
get_market_summary,
get_portfolio_breakdown,
get_sector_overview,
compare_outcomes,
calculate_spread,
)
Find events closing in the next 24 hours:
closing = await get_closing_soon(client, hours=24, category="sports")
for event in closing.data:
print(f"{event.title} — closes {event.closing_date}")
Get high-volume events:
active = await get_high_volume_markets(client, min_volume=100_000)
print(f"{len(active.data)} events above 100k volume")
Consolidated event view (event + all order books):
summary = await get_market_summary(client, event_id="evt_...")
print(f"{summary.data.event.title}: {len(summary.data.order_books)} order books")
Combined portfolio + wallet:
bd = await get_portfolio_breakdown(client)
print(f"Portfolio: {bd.data.portfolio_current_value}")
print(f"Wallet assets: {len(bd.data.assets)}")
Compare spreads across outcomes:
comparisons = await compare_outcomes(
client, ["outcome_id_1", "outcome_id_2", "outcome_id_3"]
)
for c in comparisons.data:
print(f"{c.outcome_id}: spread={c.spread}")
Aggregate view of a market sector:
sector = await get_sector_overview(client, "crypto")
print(f"{sector.data.category}: {sector.data.event_count} events, "
f"{sector.data.total_volume:.0f} total volume")
Bid-ask spread for a single outcome:
spread = await calculate_spread(client, "outcome_id")
print(f"Spread: {spread.data.spread} ({spread.data.best_bid} / {spread.data.best_ask})")
Data Science Quickstart
Every response is a Pydantic model, so it drops straight into pandas with
model_dump() — no manual JSON wrangling.
Events into a DataFrame
import pandas as pd
from bayse_markets import BayseClient
async with BayseClient(public_key=PK, secret_key=SK) as client:
events = await client.list_events(page=1, size=50, status="open")
df = pd.DataFrame([e.model_dump() for e in events.data.events])
print(df[["title", "category", "total_volume", "liquidity"]].sort_values(
"total_volume", ascending=False
).head())
Plot price history with matplotlib
import matplotlib.pyplot as plt
import pandas as pd
history = await client.get_price_history(event_id="evt_...", time_period="1W")
for market_id, points in history.data.items():
df = pd.DataFrame([p.model_dump() for p in points])
df.plot(x="timestamp", y="price", title=f"Market {market_id}")
plt.show()
Feed portfolio data into a model
portfolio = await client.get_portfolio()
df = pd.DataFrame([b.model_dump() for b in portfolio.data.outcome_balances])
# e.g. total exposure and unrealized P&L per position, ready for
# whatever sizing/risk logic your strategy uses
df["unrealized_pnl"] = df["current_value"] - df["cost"]
print(df[["outcome", "balance", "cost", "current_value", "unrealized_pnl"]])
Logging
The SDK uses Python's standard logging module with a NullHandler (silent by default).
To inspect SDK internals (requests, retries, trace IDs):
import logging
sdk_logger = logging.getLogger("bayse_markets")
sdk_logger.setLevel(logging.DEBUG)
sdk_logger.addHandler(logging.StreamHandler())
Sensitive headers (authorization, x-signature, x-public-key, etc.) are
automatically redacted from log output.
Reliability & Retries
Retries are method-aware. A 502 on a GET is worth replaying. A 502 on a
POST /orders may mean the order was accepted and only the response was lost —
replaying it risks a duplicate. So the SDK does not replay it.
| Request | Retried on |
|---|---|
GET / HEAD / OPTIONS |
429, 500, 502, 503, 504 |
DELETE / PUT |
429, 500, 502, 503, 504 |
POST |
nothing — never retried |
POST with an Idempotency-Key |
429, 500, 502, 503, 504 |
Backoff is exponential with jitter, capped at max_delay. Every retry of a
non-idempotent request logs at WARNING with the trace ID and attempt number —
it is the one event you want in your scrollback without having enabled debug
logging in advance.
Why 429 is not retried on a POST. It would be safe only if the rate
limiter sits strictly in front of order acceptance, which no client can verify.
If that assumption is wrong, the cost at the default max_retries=5 is six live
orders. Opt in if you know your own infrastructure:
RetryConfig(retry_unsafe_on_statuses=(429,))
Idempotency
The batch endpoints accept an idempotency_key and the server genuinely
deduplicates on it — a replay returns the original result instead of acting
twice. Supplying one re-enables full 5xx retries for that call.
result = await client.batch_place_orders(
body={"orders": [{...}]},
idempotency_key="order-2026-07-29-0001", # your own unique string
)
Verified against production: the same key with an identical body returns the
original order id and creates one order; two distinct keys with the same body
create two. The replayed response is indistinguishable from the first — a normal
200, not an error.
place_orderandcancel_orderdo not support idempotency keys. The API accepts the header on those routes and silently ignores it, so the SDK does not offer the parameter — accepting a key and dropping it would be worse than not having one. If you need a retry-safe write, usebatch_place_orderswith a batch of one. Otherwise, treat a failed single write as unresolved and reconcile withlist_orders(currency=...)before re-sending.
Trace IDs
Every request carries an x-trace-id, auto-generated as
{session_id}-{sequence:06d}. Quote it when reporting an issue to Bayse.
resp = await client.list_events(page=1, size=10)
print(resp.trace_id) # e.g. "77f08ca6d00d-000003"
# Override per call to correlate with your own logs
await client.get_portfolio(trace_id="reconcile-run-42")
Pin session_id so a bot's traces stay greppable across restarts:
BayseClient(..., trace_config=TraceConfig(session_id="marketmaker-prod"))
Error Handling
Every API error raises a subclass of BayseError, carrying error_code,
status_code, response_headers, and timestamp.
| Exception | Status | Notes |
|---|---|---|
InvalidSignatureError |
401 | Signature mismatch — check the secret key. |
TimestampExpiredError |
401 | Clock skew beyond the 5-minute window. |
UnauthorizedError |
401/403 | Key missing or lacks permission. |
NotFoundError |
404 | Resource does not exist. |
ValidationError |
422 | Request rejected — read .message. |
RateLimitError |
429 | Carries .retry_after_seconds when the header is present. |
InternalServerError |
500 | Server-side. |
NetworkError |
— | Transport-level; never reached the API layer. |
NetworkError tells you whether the request left your machine, which is the
difference between safely re-sending a write and risking a duplicate:
from bayse_markets.exceptions import NetworkError
try:
await client.place_order(...)
except NetworkError as exc:
if exc.request_sent is False:
... # connect-phase failure: the server never saw it, safe to re-send
else:
... # None = unknown. The order may exist. Reconcile, do not re-send.
False means a connect-phase failure (ConnectError, ConnectTimeout,
PoolTimeout, ProxyError) — definitively never delivered. None means the
connection was live when it broke, so the outcome is genuinely unknown. The
original httpx exception is preserved on .original_exception.
Known API Behaviours
Confirmed against the live API. These are venue behaviours, not SDK bugs — some contradict Bayse's published docs.
-
list_orders()returns an empty page unless you passcurrency. A missingcurrencyis not "all currencies" — it is a200with zero results and well-formed pagination. An account holding 60 NGN orders looks flat. The SDK logs a WARNING when this happens, but never use a bare call to conclude an account is empty. Values are case-sensitive. -
stp_modesilently falls back toSKIPon any unrecognised value. No error, no warning. The real set isSKIP(default),CANCEL_OLDEST,CANCEL_NEWEST,CANCEL_BOTH. A typo leaves a CLOB order with no self-trade protection while reading like working code. -
max_slippageaccepts 0–0.50, not the documented 0.00–1.00, and is never echoed back in any response — so you cannot confirm afterwards which bound was applied. Whether the engine enforces it at fill time is unverified. -
Minimum order amount is 100 in the quote currency.
-
Orders route on
outcome_idalone.marketIdis accepted but not validated; the market is derived server-side from the outcome. Don't rely on it as a safety check. -
Order statuses are lowercase:
pending,open,partial_filled,filled,cancelled,rejected,expired. Thestatusfilter accepts all of these exceptpending, so a status-by-status sweep silently misses pending orders. -
outcomemeans different things on different routes. The read routes sendoutcomeId+outcomeLabel; the place route sendsoutcomeholding a UUID. The SDK normalises both to.outcome_id. Note thatPlacedOrder.typeholds the side — prefer.sideand.order_type, which are consistent everywhere.
Design Notes
Typed everywhere. Every endpoint returns BayseResponse[T] where T is a Pydantic model. No raw dictionaries, no guesswork about field names. Your editor's autocomplete works.
Snake_case fields. The API speaks camelCase (createdAt, filledSize). The SDK translates to snake_case (created_at, filled_size). You can still use the raw API field via model.model_dump(by_alias=True) if needed.
Separate auth domains. BayseClient uses HMAC-signed requests with API keys. UserClient uses email/password + session tokens. Two clients, two auth models — no mixing concerns.
UserClient is a bootstrap tool. Use it once to create API keys, then never touch it again. BayseClient is the permanent workhorse.
One-time secret keys. When creating or rotating API keys, the secret_key is only returned once. Save it immediately.
License
MIT — see LICENSE. This project is unaffiliated with Bayse; "Bayse Markets" refers to the third-party API it wraps.
Full API reference at docs.bayse.markets.
Release files for bayse-markets 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bayse_markets-0.3.1.tar.gz | 56.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bayse_markets-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 107.8 kB
Release files / bayse_markets-0.3.1.tar.gz
| Download URL | bayse_markets-0.3.1.tar.gz |
|---|---|
| Size | 56.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
554d7cd724e10b0ca860f0ea30856733585da22b00f80dc8065e19e826c99e1c
|
|
BLAKE2b-256 checksum How to use checksums |
cdf09971925822515109cad9030b7f454e810325ca1dd004277531a7dba9550c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / bayse_markets-0.3.1-py3-none-any.whl
| Download URL | bayse_markets-0.3.1-py3-none-any.whl |
|---|---|
| Size | 51.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8de28fedc35d7f3172523d1ea9707a3cea618fdca325775bb3d50c9933c6b0c4
|
|
BLAKE2b-256 checksum How to use checksums |
6f0f98fd7b09a3a163e60bff7f700dddc97ac2e3146163320cd99295cf54a757
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log