Add your description here
Project description
Kuru Market Maker SDK (Python)
A Python SDK for building market maker bots on the Kuru orderbook protocol.
Features
- Batch Cancel/Replace - Atomically cancel and place orders in a single transaction via the MM Entrypoint
- Order Lifecycle Tracking - Track orders from creation through fills and cancellations via on-chain events
- Real-time Orderbook Feed - WebSocket client for live best bid/ask with auto-reconnection
- Margin Account Integration - Deposit, withdraw, and query margin balances
- EIP-7702 Authorization - Automatic MM Entrypoint authorization for your EOA
- Gas Optimization - EIP-2930 access list support to reduce gas costs
Installation
pip install kuru-sdk-py
# or
uv add kuru-sdk-py
Create a .env file:
# Required
PRIVATE_KEY=0x...
MARKET_ADDRESS=0x...
# Optional endpoints (defaults exist)
RPC_URL=https://rpc.monad.xyz/
RPC_WS_URL=wss://rpc.monad.xyz/
KURU_WS_URL=wss://ws.kuru.io/
KURU_API_URL=https://api.kuru.io/
# Optional WebSocket behavior
KURU_RPC_LOGS_SUBSCRIPTION=monadLogs # Set to logs for commited-state streaming
# Optional trading defaults
KURU_POST_ONLY=true
KURU_AUTO_APPROVE=true
KURU_USE_ACCESS_LIST=true
Example
You can find an example market making bot here — a skew-based quoting strategy with event-driven order tracking.
Core Concepts
Kuru market making interacts with three on-chain components:
- Orderbook contract (
MARKET_ADDRESS): holds the limit orderbook and emitsOrderCreated,OrdersCanceled, andTradeevents. - Margin account contract: holds your trading balances. Orders consume margin balances, not wallet balances.
- MM Entrypoint (EIP-7702 delegation): your EOA is authorized to delegate to the MM Entrypoint contract, enabling batch cancel and place operations in a single transaction.
The SDK provides:
KuruClient: the main facade for execution, event listeners, and orderbook streaming.User: deposits/withdrawals, approvals, and EIP-7702 authorization.Order: your local representation of a limit order or cancel.
Market Making Guide
A typical market making bot has four concurrent loops:
- Market data ingestion - subscribe to the orderbook WebSocket for best bid/ask.
- Quote generation - decide where to quote (spreads, levels, sizes) based on your model + inventory.
- Execution - atomically cancel + place quotes using
client.place_orders(). - Order lifecycle & fills - listen to on-chain events to track placements, fills, and cancellations.
The SDK handles (1), (3), and (4). You supply (2).
Step 1 - Create a client
import os
from dotenv import load_dotenv
from kuru_sdk_py.client import KuruClient
from kuru_sdk_py.configs import ConfigManager
load_dotenv()
configs = ConfigManager.load_all_configs(
market_address=os.environ["MARKET_ADDRESS"],
fetch_from_chain=True,
)
client = await KuruClient.create(**configs)
fetch_from_chain=True reads the market's on-chain params and sets price_precision, size_precision, tick_size, and token addresses/decimals/symbols automatically.
Step 2 - Fund margin
Orders are backed by margin balances. Deposit base and/or quote before quoting:
await client.user.deposit_base(10.0, auto_approve=True)
await client.user.deposit_quote(500.0, auto_approve=True)
margin_base_wei, margin_quote_wei = await client.user.get_margin_balances()
If you quote both sides, keep both base and quote margin funded; otherwise your orders will revert.
Step 3 - Start the client
client.start() performs EIP-7702 authorization and connects to the RPC WebSocket for on-chain event tracking.
Set an order callback to track your order lifecycle:
from kuru_sdk_py.manager.order import Order, OrderStatus
active_cloids: set[str] = set()
async def on_order(order: Order) -> None:
if order.status in (OrderStatus.ORDER_PLACED, OrderStatus.ORDER_PARTIALLY_FILLED):
active_cloids.add(order.cloid)
if order.status in (OrderStatus.ORDER_CANCELLED, OrderStatus.ORDER_FULLY_FILLED):
active_cloids.discard(order.cloid)
client.set_order_callback(on_order)
await client.start()
Alternatively, read from client.orders_manager.processed_orders_queue instead of using callbacks.
Step 4 - Subscribe to real-time orderbook data
from kuru_sdk_py.feed.orderbook_ws import KuruFrontendOrderbookClient, FrontendOrderbookUpdate
best_bid: float | None = None
best_ask: float | None = None
async def on_orderbook(update: FrontendOrderbookUpdate) -> None:
global best_bid, best_ask
if update.b:
best_bid = update.b[0][0]
if update.a:
best_ask = update.a[0][0]
client.set_orderbook_callback(on_orderbook)
await client.subscribe_to_orderbook()
WebSocket price/size units: Prices and sizes in FrontendOrderbookUpdate are pre-normalized to human-readable floats. No manual conversion is needed.
Step 5 - Cancel and place quotes
Build a grid of orders, cancel stale ones, and send them in a single batch transaction:
import time
from kuru_sdk_py.manager.order import Order, OrderType, OrderSide
def build_grid(mid: float) -> list[Order]:
ts = int(time.time() * 1000)
spread_bps = 10 # 0.10%
size = 5.0
bid = mid * (1 - spread_bps / 10_000)
ask = mid * (1 + spread_bps / 10_000)
return [
Order(cloid=f"bid-{ts}", order_type=OrderType.LIMIT, side=OrderSide.BUY, price=bid, size=size),
Order(cloid=f"ask-{ts}", order_type=OrderType.LIMIT, side=OrderSide.SELL, price=ask, size=size),
]
orders = []
orders += [Order(cloid=c, order_type=OrderType.CANCEL) for c in active_cloids]
orders += build_grid(mid=100.0)
txhash = await client.place_orders(
orders,
post_only=True, # maker-only (recommended)
price_rounding="default", # buy rounds down, sell rounds up
)
Cancel all orders
To cancel all active orders for the market (useful for circuit breakers or graceful shutdown):
await client.cancel_all_active_orders_for_market()
This cancels every order you have on the book for this market in a single transaction, regardless of whether you're tracking them locally.
Tick size and rounding
On-chain prices must align to market_config.tick_size. When you pass float prices to place_orders(), the SDK converts them to integers using price_precision and then rounds to tick size.
price_rounding options:
"default"- round down for buys and up for sells (recommended)"down"- round down for both sides"up"- round up for both sides"none"- no tick rounding (only if you already quantize yourself)
Step 6 - React to fills and inventory
The SDK delivers order status updates via the callback or queue:
ORDER_PLACED- confirmed on bookORDER_PARTIALLY_FILLED- size reducedORDER_FULLY_FILLED- size goes to 0ORDER_CANCELLED- removed from book
Typical market maker reactions:
- Record fills and PnL
- Adjust inventory targets
- Skew spreads based on inventory (long base -> tighten asks, widen bids)
- Refresh quotes more aggressively after fills
Compute inventory from margin balances:
margin_base_wei, margin_quote_wei = await client.user.get_margin_balances()
base = margin_base_wei / (10 ** market_config.base_token_decimals)
quote = margin_quote_wei / (10 ** market_config.quote_token_decimals)
Order Types
Limit Orders
from kuru_sdk_py.manager.order import Order, OrderType, OrderSide
Order(
cloid="my-bid-1",
order_type=OrderType.LIMIT,
side=OrderSide.BUY,
price=100.0,
size=1.0,
)
Market Orders
await client.place_market_buy(quote_amount=100.0, min_amount_out=0.9)
await client.place_market_sell(size=1.0, min_amount_out=90.0)
Cancel Orders
Order(cloid="my-bid-1", order_type=OrderType.CANCEL)
Batch Updates
orders = [
Order(cloid="bid-1", order_type=OrderType.LIMIT, side=OrderSide.BUY, price=99.0, size=1.0),
Order(cloid="ask-1", order_type=OrderType.LIMIT, side=OrderSide.SELL, price=101.0, size=1.0),
Order(cloid="old-bid", order_type=OrderType.CANCEL),
]
txhash = await client.place_orders(orders, post_only=True)
WebSocket Feeds
The SDK provides two standalone WebSocket clients for real-time orderbook data. Both can be used independently of KuruClient — no wallet or private key required.
KuruFrontendOrderbookClient (orderbook_ws)
Connects to Kuru's frontend orderbook WebSocket. Delivers a full L2 snapshot on connect, then incremental updates with events (trades, order placements, cancellations).
- Prices and sizes are pre-normalized to human-readable floats.
- Each update may contain
b(bids) anda(asks) with all current levels at that price, plus aneventslist describing what changed.
import asyncio
from kuru_sdk_py.configs import ConfigManager
from kuru_sdk_py.feed.orderbook_ws import KuruFrontendOrderbookClient, FrontendOrderbookUpdate
market_config = ConfigManager.load_market_config(market_address="0x...", fetch_from_chain=True)
connection_config = ConfigManager.load_connection_config()
update_queue: asyncio.Queue[FrontendOrderbookUpdate] = asyncio.Queue()
client = KuruFrontendOrderbookClient(
ws_url=connection_config.kuru_ws_url,
market_address=market_config.market_address,
update_queue=update_queue,
size_precision=market_config.size_precision,
)
async with client:
while True:
update = await update_queue.get()
if update.b:
best_bid = update.b[0][0] # Already a float
if update.a:
best_ask = update.a[0][0] # Already a float
You can also subscribe via KuruClient instead of using the client directly:
client.set_orderbook_callback(on_orderbook)
await client.subscribe_to_orderbook()
ExchangeWebsocketClient (exchange_ws)
Connects to the exchange WebSocket in Binance-compatible format. Delivers incremental depth updates (depthUpdate) and optionally Monad-enhanced updates with blockchain state (monadDepthUpdate).
- Messages are binary (JSON serialized to bytes).
- Prices and sizes are pre-normalized to human-readable floats.
MonadDepthUpdateincludesblockNumber,blockId, andstate("proposed"|"committed"|"finalized").
Key difference from orderbook_ws: The exchange WebSocket sends incremental delta updates only — b and a contain only the price levels that changed, not the full book. A size of 0.0 means that level was removed. You must maintain a local orderbook and apply each delta.
import asyncio
from kuru_sdk_py.configs import ConfigManager
from kuru_sdk_py.feed.exchange_ws import ExchangeWebsocketClient, DepthUpdate, MonadDepthUpdate
market_config = ConfigManager.load_market_config(market_address="0x...", fetch_from_chain=True)
connection_config = ConfigManager.load_connection_config()
update_queue = asyncio.Queue()
client = ExchangeWebsocketClient(
ws_url=connection_config.exchange_ws_url,
market_config=market_config,
update_queue=update_queue,
)
# Local orderbook — must be seeded and maintained manually
orderbook = {"bids": {}, "asks": {}}
async with client:
while True:
update = await update_queue.get()
# Apply bid deltas (prices and sizes are pre-normalized floats)
for price, size in update.b:
if size == 0.0:
orderbook["bids"].pop(price, None) # Level removed
else:
orderbook["bids"][price] = size # Level added or updated
# Apply ask deltas
for price, size in update.a:
if size == 0.0:
orderbook["asks"].pop(price, None) # Level removed
else:
orderbook["asks"][price] = size # Level added or updated
# For MonadDepthUpdate, blockchain state is also available:
if isinstance(update, MonadDepthUpdate):
print(f"Block {update.blockNumber} ({update.state})")
Choosing between the two
KuruFrontendOrderbookClient |
ExchangeWebsocketClient |
|
|---|---|---|
| Initial snapshot | Full L2 book on connect | None (delta-only) |
| Update style | Full levels at changed prices + events | Incremental deltas |
| Local book required | No | Yes |
| Event detail (trades, orders) | Yes (events field) |
No |
| Blockchain state | No | Yes (monad variant) |
| Message format | Text JSON | Binary JSON |
Configuration
The SDK uses ConfigManager to load configuration from environment variables with sensible defaults. See the Environment Variables section above for the full list.
For advanced configuration (custom timeouts, reconnection behavior, gas settings, presets), see examples/config_examples.py.
from kuru_sdk_py.configs import ConfigManager, ConfigPresets
# One-liner: load everything from env vars
configs = ConfigManager.load_all_configs(
market_address=os.environ["MARKET_ADDRESS"],
fetch_from_chain=True,
)
client = await KuruClient.create(**configs)
# Or use presets for common scenarios
preset = ConfigPresets.conservative() # Longer timeouts, more retries (production)
preset = ConfigPresets.aggressive() # Shorter timeouts, fewer retries (HFT)
preset = ConfigPresets.testnet() # Optimized for slower testnets
Production Guidance
Use a dedicated RPC
The default public endpoints can be rate-limited. For production, use a dedicated RPC provider via RPC_URL and RPC_WS_URL.
Quote cadence and gas
Batch cancel/replace every second is expensive on-chain. Common approaches:
- Update only when mid price moves beyond a threshold
- Update at a slower cadence (e.g., 5-15s) unless volatility spikes
- Use fewer levels or smaller grids
- Enable EIP-2930 access list optimization (
KURU_USE_ACCESS_LIST=true)
If you see "out of gas" or "gas too low" transaction failures, increase the gas buffer multiplier:
KURU_GAS_BUFFER_MULTIPLIER=1.3 # default is 1.2; try 1.3 to 1.5
Safety checks
- Stale data guard - don't quote if your market data feed is older than N milliseconds
- Min/max size - ensure order sizes meet market constraints (or the tx will revert)
- Balance guard - ensure margin balances can support your outstanding orders
- Circuit breakers - stop quoting on repeated failures, disconnects, or extreme spreads. Use
await client.cancel_all_active_orders_for_market()to cancel all outstanding orders when the circuit breaker triggers
Examples
# End-to-end market making bot (requires PRIVATE_KEY + MARKET_ADDRESS)
PYTHONPATH=. uv run python examples/simple_market_making_bot.py
# Read-only frontend orderbook stream (full snapshots, no wallet required)
PYTHONPATH=. uv run python examples/get_orderbook_ws.py
# Read-only exchange orderbook stream (Binance-compatible delta updates, no wallet required)
PYTHONPATH=. uv run python examples/get_exchange_orderbook_ws.py
# Repeated batch placement + cancels
PYTHONPATH=. uv run python examples/place_many_orders.py
Testing
uv run pytest tests/ -v
Requirements
- Python >= 3.10
- Dependencies managed via uv (see
pyproject.toml)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kuru_sdk_py-0.1.6.tar.gz.
File metadata
- Download URL: kuru_sdk_py-0.1.6.tar.gz
- Upload date:
- Size: 135.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36944043a3a2fa04b7033b5b5976512212735546e5b23554e998e55f08bb18cc
|
|
| MD5 |
34b3238225b4bfec02bf1faecfc288bc
|
|
| BLAKE2b-256 |
0301b810b72029bb8eb7b1b7d23eb1e345441bdd87395496c4403562b09ca23e
|
File details
Details for the file kuru_sdk_py-0.1.6-py3-none-any.whl.
File metadata
- Download URL: kuru_sdk_py-0.1.6-py3-none-any.whl
- Upload date:
- Size: 129.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c093963083523d9fee44c0031cc10a00c6e4426ecd561ae220b52f18ad976cf1
|
|
| MD5 |
f2c767b6d8129063edede026bbb5d8bd
|
|
| BLAKE2b-256 |
56d112cccb277d2114538a01cef6f64014a6cca139034aead2e4fd64c744b548
|