BulkHttpClient provides synchronous request/response access to the Bulk exchange over HTTP.
It covers three categories of endpoints:
- Market data — public, unsigned (tickers, order books, candles)
- Account queries — public, unsigned (positions, open orders, fills)
- Trading & settings — private, signed (orders, cancels, leverage, agent wallets)
BulkHttpClient(
base_url: str = "https://exchange-api2.bulk.trade/api/v1",
private_key: Optional[str] = None,
timeout: int = 10
)| Parameter | Type | Default | Description |
|---|---|---|---|
base_url |
str |
"https://exchange-api2.bulk.trade/api/v1" |
HTTP endpoint |
private_key |
Optional[str] |
None |
Base58-encoded private key. Required for trading and settings operations |
timeout |
int |
10 |
Per-request timeout in seconds |
No private key is needed for public endpoints. Attempts to call any trading or
settings method on an unauthenticated client will raise a ValueError immediately.
from bulk_api import BulkHttpClient
client = BulkHttpClient()import os
from bulk_api import BulkHttpClient
client = BulkHttpClient(private_key=os.environ["BULK_PRIVATE_KEY"])
print(f"Trading as: {client.signer.public_key}")Returns metadata for all available markets.
info = client.get_exchange_info()
for symbol, market in info["markets"].items():
print(f"{symbol}: tick={market['tickSize']} lot={market['lotSize']}")Returns dict with keys:
| Key | Description |
|---|---|
symbols |
List of available market symbols |
markets |
Dict of symbol → market detail (tick size, lot size, etc.) |
Returns the current ticker and statistics for a single market.
ticker = client.get_ticker("BTC-USD")
print(f"mark={ticker['markPrice']} funding={ticker['fundingRate']:.6f}")Returns dict with keys:
| Key | Description |
|---|---|
symbol |
Market symbol |
lastPrice |
Last traded price |
markPrice |
Current mark price |
oraclePrice |
Current oracle price |
fundingRate |
Current funding rate |
openInterest |
Total open interest |
volume24h |
24-hour trading volume |
highPrice24h / lowPrice24h |
24-hour high / low |
priceChange24h / priceChangePercent24h |
24-hour price change |
Returns historical OHLCV candles.
| Parameter | Type | Default | Description |
|---|---|---|---|
symbol |
str |
— | Market symbol, e.g. "BTC-USD" |
interval |
Literal[...] |
— | "1m" "5m" "15m" "30m" "1h" "4h" "1d" "1w" |
start_time |
Optional[int] |
None |
Start timestamp in milliseconds |
end_time |
Optional[int] |
None |
End timestamp in milliseconds |
limit |
int |
500 |
Max candles to return (max 1000) |
# Last 100 hourly candles
candles = client.get_klines("BTC-USD", "1h", limit=100)
for c in candles:
print(f"t={c['t']} o={c['o']} h={c['h']} l={c['l']} c={c['c']} v={c['v']}")Each candle is a dict with compact keys: t (open time ms), T (close time ms),
o, h, l, c (OHLC prices), v (volume), n (trade count).
Returns an L2 order book snapshot.
| Parameter | Type | Default | Description |
|---|---|---|---|
symbol |
str |
— | Market symbol |
nlevels |
int |
20 |
Price levels per side (max 1000) |
aggregation |
Optional[float] |
None |
Optional price grouping/bucketing |
book = client.get_orderbook("BTC-USD", nlevels=5)
bids, asks = book["levels"]
print(f"best bid={bids[0][0]} best ask={asks[0][0]} spread={asks[0][0] - bids[0][0]:.2f}")Each level is a [price, size, num_orders] list.
Account queries are public — they take a base58 public key string and require no signature.
Returns the complete account state: margin, open positions, open orders, and leverage settings.
account = client.get_full_account("YOUR_PUBKEY_BASE58")
full = account["fullAccount"]
print(f"positions: {len(full['positions'])}")
print(f"open orders: {len(full['openOrders'])}")Returns all resting orders for an account.
orders = client.get_open_orders("YOUR_PUBKEY_BASE58")
for entry in orders:
o = entry["openOrder"]
print(f"oid={o['orderId']} {o['coin']} px={o['price']} sz={o['size']}")Returns up to 5 000 recent trade fills.
fills = client.get_fills("YOUR_PUBKEY_BASE58")
for entry in fills:
f = entry["fills"]
print(f"{f['symbol']} {'buy' if f['isBuy'] else 'sell'} {f['amount']} @ {f['price']}")Returns up to 5 000 closed position records with realised P&L.
history = client.get_position_history("YOUR_PUBKEY_BASE58")
for entry in history:
p = entry["positions"]
print(f"{p['symbol']} pnl={p['realizedPnl']:.2f} closed={p['closeReason']}")All trading endpoints require a private_key to have been passed at construction.
A ValueError is raised immediately if the client has no signer.
place_orders accepts a list of action objects. Import them from bulk_api.messages:
from bulk_api.messages import LimitOrder, MarketOrder, CancelOrder, CancelAll
from bulk_api.common import Side, TimeInForce| Class | Description |
|---|---|
LimitOrder |
Passive limit order |
MarketOrder |
Aggressive market order |
CancelOrder |
Cancel a specific order by ID |
CancelAll |
Cancel all orders, optionally filtered to specific symbols |
Submits a list of actions as a single signed transaction. The exchange processes them atomically in order.
| Parameter | Type | Description |
|---|---|---|
txns |
List[LimitOrder | MarketOrder | CancelOrder | CancelAll] |
Actions to include |
nonce |
Optional[int] |
Override nonce (defaults to time.time_ns()) |
Returns the raw exchange JSON response. Use OrderResponse.from_api() to parse
individual action results (see §6).
from bulk_api.messages import LimitOrder
from bulk_api.common import Side, TimeInForce
resp = client.place_orders([
LimitOrder(
symbol="BTC-USD",
side=Side.BUY,
price=95_000.0,
size=0.1,
time_in_force=TimeInForce.GTC,
reduce_only=False,
)
])LimitOrder fields:
| Field | Type | Default | Description |
|---|---|---|---|
symbol |
str |
— | Market symbol |
side |
Side |
— | Side.BUY or Side.SELL |
price |
float |
— | Limit price |
size |
float |
— | Order quantity |
time_in_force |
TimeInForce |
TimeInForce.GTC |
GTC | IOC | ALO |
reduce_only |
bool |
False |
If True, only reduces an existing position |
from bulk_api.messages import MarketOrder
resp = client.place_orders([
MarketOrder(symbol="ETH-USD", side=Side.SELL, size=1.0)
])MarketOrder fields:
| Field | Type | Default | Description |
|---|---|---|---|
symbol |
str |
— | Market symbol |
side |
Side |
— | Side.BUY or Side.SELL |
size |
float |
— | Order quantity |
reduce_only |
bool |
False |
If True, only reduces an existing position |
from bulk_api.messages import CancelOrder
resp = client.place_orders([
CancelOrder(symbol="BTC-USD", oid="EXISTING_ORDER_ID_BASE58")
])from bulk_api.messages import CancelAll
# Cancel all open orders across every symbol
resp = client.place_orders([CancelAll(symbols=[])])
# Cancel only BTC-USD and ETH-USD
resp = client.place_orders([CancelAll(symbols=["BTC-USD", "ETH-USD"])])All actions in a single place_orders call are bundled into one signed transaction:
# Ladder: three bids at descending price levels
resp = client.place_orders([
LimitOrder(symbol="BTC-USD", side=Side.BUY, price=95_000.0, size=0.05),
LimitOrder(symbol="BTC-USD", side=Side.BUY, price=94_000.0, size=0.05),
LimitOrder(symbol="BTC-USD", side=Side.BUY, price=93_000.0, size=0.05),
])# Atomic replace: cancel + re-place in one transaction
resp = client.place_orders([
CancelOrder(symbol="BTC-USD", oid=existing_order_id),
LimitOrder(symbol="BTC-USD", side=Side.BUY, price=94_500.0, size=0.1),
])Updates maximum leverage for one or more markets in a single transaction.
client.update_leverage([
("BTC-USD", 20.0),
("ETH-USD", 10.0),
])leverage_settings is a list of (symbol, max_leverage) tuples.
Authorises or revokes an agent wallet. An authorised agent can sign transactions on behalf of the account without holding its private key.
| Parameter | Type | Default | Description |
|---|---|---|---|
agent_pubkey |
str |
— | Agent's base58 public key |
delete |
bool |
False |
True to revoke, False to authorise |
# Authorise an agent
client.manage_agent_wallet("AGENT_PUBKEY_BASE58", delete=False)
# Revoke an agent
client.manage_agent_wallet("AGENT_PUBKEY_BASE58", delete=True)Requests testnet funds. All accounts receive a standard top-up; whitelisted accounts
may specify a custom amount.
# Standard top-up for the signer's own account
client.request_faucet()
# Specific amount (whitelisted accounts only)
client.request_faucet(amount=10_000.0)
# Top-up for another account
client.request_faucet(user="OTHER_PUBKEY_BASE58")Adds or removes an account from the faucet whitelist. Testnet admin only.
# Add to whitelist
client.whitelist_faucet("TARGET_PUBKEY_BASE58", whitelist=True)
# Remove from whitelist
client.whitelist_faucet("TARGET_PUBKEY_BASE58", whitelist=False)Parse raw exchange responses from place_orders using OrderResponse.from_api(),
which returns a list — one entry per action submitted.
from bulk_api.messages import OrderResponse
raw = client.place_orders([...])
responses = OrderResponse.from_api(raw)
for resp in responses:
if resp.is_error():
print(f"rejected: {resp.message}")
else:
print(f"oid={resp.order_id} status={resp.status}")| Field | Type | Description |
|---|---|---|
order_id |
Optional[str] |
Base58 order ID (present for placements) |
status |
OrderStatus |
See status enum below |
message |
Optional[str] |
Error detail when status is an error variant |
meta |
dict |
Raw response body from the exchange |
| Value | String | Meaning |
|---|---|---|
RESTING |
"resting" / "placed" |
Limit order is live on the book |
WORKING |
"working" |
Order is being processed |
FILLED |
"filled" |
Fully filled immediately |
PARTIALLY_FILLED |
"partiallyFilled" |
Partially filled, remainder resting |
CANCELLED |
"cancelled" |
Cancelled normally |
CANCELLED_IOC |
"cancelledIOC" |
IOC order cancelled (unfilled remainder) |
CANCELLED_RISKLIMIT |
"cancelledRiskLimit" |
Cancelled by risk engine |
CANCELLED_SELFCROSSING |
"cancelledSelfCrossing" |
Cancelled to prevent self-cross |
CANCELLED_REDUCEONLY |
"cancelledReduceOnly" |
Cancelled: reduce-only constraint |
REJECTED_CROSSING |
"rejectedCrossing" |
Would cross own resting orders |
REJECTED_DUPLICATE |
"rejectedDuplicate" |
Duplicate order ID |
REJECTED_RISKLIMIT |
"rejectedRiskLimit" |
Would exceed risk/leverage limits |
REJECTED_INVALID |
"rejectedInvalid" |
Malformed or invalid parameters |
CANCEL_REJECT |
"cancelAllRejected" / "cancelOneRejected" |
Cancel request itself was rejected |
ERROR |
"error" |
Generic exchange error |
resp.is_error() # True for ERROR, REJECTED_* variants
resp.status.is_terminal() # True if the order can no longer change state
resp.status.is_cancelled() # True for any CANCELLED_* variant
resp.status.is_placed() # True if the order is live (RESTING or WORKING)from bulk_api import BulkHttpClient
client = BulkHttpClient()
ticker = client.get_ticker("BTC-USD")
print(f"BTC mark={ticker['markPrice']} funding={ticker['fundingRate']:.6f}")
book = client.get_orderbook("BTC-USD", nlevels=5)
bids, asks = book["levels"]
print(f"best bid={bids[0][0]} best ask={asks[0][0]}")
candles = client.get_klines("BTC-USD", "1h", limit=5)
print(f"latest close={candles[-1]['c']}")import os
from bulk_api import BulkHttpClient
from bulk_api.messages import LimitOrder, CancelAll, OrderResponse
from bulk_api.common import Side, TimeInForce
client = BulkHttpClient(private_key=os.environ["BULK_PRIVATE_KEY"])
# Ladder: three bids at descending price levels
raw = client.place_orders([
LimitOrder(symbol="BTC-USD", side=Side.BUY, price=95_000.0, size=0.05),
LimitOrder(symbol="BTC-USD", side=Side.BUY, price=94_000.0, size=0.05),
LimitOrder(symbol="BTC-USD", side=Side.BUY, price=93_000.0, size=0.05),
])
responses = OrderResponse.from_api(raw)
if any(r.is_error() for r in responses):
print("One or more placements failed — cancelling all")
for r in responses:
if r.is_error():
print(f" {r.message}")
client.place_orders([CancelAll(symbols=["BTC-USD"])])
else:
for r in responses:
print(f"placed oid={r.order_id} status={r.status}")| Feature | Python | Rust |
|---|---|---|
| Construction | BulkHttpClient(base_url, private_key, timeout) |
BulkHttpClient::with_url(base_url, private_key) or BulkHttpClient::new(&config) |
| Action types | LimitOrder, MarketOrder, CancelOrder, CancelAll dataclasses |
Action enum variants |
| Batch submit | place_orders(txns) |
place_tx(actions, account, nonce) |
| Single order helpers | — (use place_orders with one item) |
place_limit_order(), place_market_order() |
| Account override | — (always uses signer's key) | account: Option<Pubkey> on every trading method |
| Nonce override | nonce param on place_orders only |
nonce: Option<u64> on every trading method |
| Response type | OrderResponse (parsed via from_api()) |
Response (returned directly) |
OrderStatus |
Rich enum with terminal/cancelled/placed helpers | String-based status field |
| Leverage input | List[Tuple[str, float]] |
HashMap<String, f64> |
| Async | No (blocking requests) |
Yes (tokio / async fn) |