HeliEx provides a REST API for trading the CURE/GRC pair (buy and sell CURE, priced in GRC). Public endpoints need no authentication. Private endpoints need an API key and an HMAC-SHA256 signature on every request.
Base URL: https://heliex.net · JSON over HTTPS · last updated 2026-09-29.
Paste this block into your assistant along with this page. Every rule below is enforced by the server.
HeliEx REST API - rules
- Base URL https://heliex.net. Pair CURE/GRC: price = GRC per 1 CURE, amount = CURE.
- Send a descriptive User-Agent header on EVERY request, public ones too (e.g.
"User-Agent: my-bot/1.0"). Default library agents such as Python-urllib may be
blocked by the site's CDN with HTTP 403 before the request reaches HeliEx.
- Numbers in responses are exact decimal STRINGS with 8 decimals (e.g. "2.00000000").
Parse with Decimal / BigNumber, never binary floats. Send numbers as strings.
- Timestamps are UTC ISO 8601 with a trailing Z, e.g. "2026-09-26T18:38:11.552Z".
- Private requests need 4 headers: X-API-Key, X-API-Timestamp (unix seconds, within
60 s of server time), X-API-Nonce (16-128 chars [A-Za-z0-9._~-], NEW for every
request), X-API-Signature = hex(HMAC_SHA256(secret, timestamp + nonce + METHOD +
path_with_query + body)). Body = the exact bytes you send ("" for GET).
- Missing nonce -> 401. Reused nonce -> 409. Too fast -> 429 + Retry-After.
- Read the X-RateLimit-Remaining / X-RateLimit-Reset headers to pace requests.
- Always send a unique client_order_id when placing orders. On a timeout or network
error, resend the SAME order with the SAME client_order_id (and a NEW nonce): you
get the original order back, never a duplicate.
- Orders are limit orders only. States: open, partial (live) / filled, cancelled (final).
- Use "filled" (not amount - remaining) for executed quantity.
- Price protection: >10% worse than market -> 400 code price_deviation_confirm_required
(resend with "confirm_price_deviation": true); >50% worse -> always rejected.
- Poll /api/orderbook (compare "sequence") and /api/trades?since_id=N. No WebSocket.
- Read-only market data for trackers/aggregators: /api/v1/* (no key, CORS *, cached
10 s, 120 requests/min per IP); see "Public Market Data API" below.
- Withdrawals: only with a key that has the Withdraw permission (needs 2FA and an IP
allowlist), only to addresses on your withdrawal whitelist (added on the website),
and only after the 24 h activation delays. POST /api/key/withdrawals with a unique
client_id; on a timeout resend the SAME client_id (new nonce) - never a duplicate.
Refusals return {"error": CODE, "detail": ...}. They may be switched off site-wide
(503 API_WITHDRAWALS_DISABLED).
- Bots without an account can skip the swap page's human check by paying a small fee
on-chain first: POST /api/swaps/fee-request (no key). See "Paid swaps for bots".
No authentication. Market data (order book, trades, stats, candles) is sent with Cache-Control: no-store, so every poll gets fresh data.
GET /api/orderbook
Top 50 price levels per side, aggregated by price. Bids are sorted highest first, asks lowest first.
{
"timestamp": "2026-09-26T18:41:12.482Z",
"last_update": "2026-09-26T18:40:58.103Z",
"sequence": 18342,
"bids": [{"price": "2.00000000", "amount": "401.70313869"}, ...],
"asks": [{"price": "4.50000000", "amount": "505.32222900"}, ...]
}
GET /api/trades?limit=50
GET /api/trades?since_id=948&limit=500
Without since_id: the most recent trades, newest first. With since_id: trades with a higher id, oldest first - poll with the highest id you have seen to never miss a trade. limit 1-500, default 50. A trade executes at the price of the order that was already resting on the book.
[
{"id": 949, "price": "4.50000000", "amount": "10.00000000", "created_at": "2026-09-26T18:38:11.552Z"}
]
GET /api/market_stats
Last trade price, 24-hour change, high and low, and 7-day volume in CURE.
{
"last_price": "4.50000000",
"daily_change_pct": "125.00",
"daily_high": "4.50000000",
"daily_low": "2.00000000",
"weekly_volume": "233.24354515"
}
GET /api/chart/candles?range=1d&interval=auto
range: 1d, 7d, 30d (default), 90d, 1y, all. interval: auto (default), 1m, 5m, 15m, 1h, 4h, 1d, 1w. At most 2,000 candles per request (a smaller interval over a long range returns HTTP 400). Times are Unix seconds (UTC); values are decimal strings. Refreshed about every 15 seconds.
{
"range": "1d", "interval_seconds": 900, "from": 1790365500, "to": 1790451900, "as_of": 1790452378,
"base_asset": "CURE", "quote_asset": "GRC",
"candles": [
{"time": 1790403300, "open": "2.00000000", "high": "2.00000000", "low": "2.00000000",
"close": "2.00000000", "volume": "0.00625000", "quote_volume": "0.01250000", "trade_count": 1}
]
}
GET /api/health
GET /api/wallet_status
/api/wallet_status reports whether each coin's wallet is online: {"online": true, "assets": {"GRC": {"online": true, "error": ""}, "CURE": {...}}}. New orders are refused (HTTP 503) while a wallet is offline.
Read-only market data for price trackers, aggregators (CoinGecko, CoinMarketCap) and dashboards. No authentication and no account data: aggregated order book levels, public trades and 24-hour statistics only. Pair CURE_GRC: base CURE, quote GRC, price = GRC per 1 CURE. Pair names are case-insensitive and may use - instead of _.
Cache-Control: public, max-age=10. Polling faster returns the same data; timestamp tells you when the snapshot was taken./api/v1 endpoints, counted separately from the website's own limit. Over the limit: HTTP 429 with a Retry-After header (seconds) and {"detail": "Rate limit exceeded", "retry_after": 12}.Access-Control-Allow-Origin: * for GET, HEAD and OPTIONS, without credentials, so a web page on any site can read these endpoints. The rest of the API is not open to other sites.trade_timestamp in seconds). A value that does not exist yet (no trades, empty book side) is null.buy means a buy order removed an ask from the book, sell means a sell order removed a bid.{"detail": "Unknown pair", "pairs": ["CURE_GRC"]}; invalid or out-of-range parameter: 422 (400 when start_time is after end_time); a temporary database problem: 503 with Retry-After.isFrozen is then 1).GET /api/v1/ticker
GET /api/v1/ticker?pair=CURE_GRC
{
"CURE_GRC": {
"pair": "CURE_GRC", "base": "CURE", "quote": "GRC",
"last_price": "2.50000000", "high_24h": "3.00000000", "low_24h": "2.00000000",
"change_24h_pct": "25.00000000",
"base_volume_24h": "3.50000000", "quote_volume_24h": "8.50000000",
"best_bid": "2.00000000", "best_ask": "2.60000000",
"timestamp": "2026-09-29T23:28:32.525Z"
}
}
GET /api/v1/orderbook?pair=CURE_GRC&depth=50
depth: price levels per side, 1-500, default 50. Levels are aggregated by price as [price, amount]; bids highest first, asks lowest first. last_update and sequence work as in /api/orderbook.
{
"pair": "CURE_GRC", "timestamp": "2026-09-29T23:28:32.525Z",
"last_update": "2026-09-29T12:00:00.250Z", "sequence": 18342,
"bids": [["2.00000000", "4.00000000"], ["1.90000000", "1.00000000"]],
"asks": [["2.60000000", "4.00000000"], ["2.75000000", "0.25000000"]]
}
GET /api/v1/trades?pair=CURE_GRC&limit=100
Most recent trades, newest first. limit 1-500, default 100. quote_volume = price × base_volume (GRC).
{
"pair": "CURE_GRC", "timestamp": "2026-09-29T23:28:32.525Z",
"trades": [
{"id": 5, "price": "2.50000000", "base_volume": "2.00000000", "quote_volume": "5.00000000",
"side": "buy", "timestamp": "2026-09-29T22:28:29.341Z"}
]
}
GET /api/v1/summary
Pairs plus the site-wide counts shown in the footer: registered people (system and swap custody accounts excluded), all trades, and active orders.
{
"exchange": "HeliEx", "timestamp": "2026-09-29T23:28:32.525Z",
"pairs": [{"pair": "CURE_GRC", "base": "CURE", "quote": "GRC", "last_price": "2.50000000"}],
"users": 3, "trades": 5, "orders": 9
}
GET /api/v1/coingecko/pairs
GET /api/v1/coingecko/tickers
GET /api/v1/coingecko/orderbook?ticker_id=CURE_GRC&depth=100
GET /api/v1/coingecko/historical_trades?ticker_id=CURE_GRC&type=buy&limit=200&start_time=1790600000&end_time=1790700000
Orderbook depth: 0 = full book (at most 500 levels per side), otherwise depth/2 levels per side (100 = 50 bids and 50 asks); timestamp = last book change in Unix milliseconds. Historical trades: type buy or sell (optional), limit 0-500 (0 = as many as allowed, 500), start_time/end_time in Unix seconds; trade_timestamp in Unix seconds.
[{"ticker_id": "CURE_GRC", "base_currency": "CURE", "target_currency": "GRC",
"last_price": "2.50000000", "base_volume": "3.50000000", "target_volume": "8.50000000",
"bid": "2.00000000", "ask": "2.60000000", "high": "3.00000000", "low": "2.00000000"}]
{"buy": [{"trade_id": 5, "price": "2.50000000", "base_volume": "2.00000000",
"target_volume": "5.00000000", "trade_timestamp": 1790720909, "type": "buy"}],
"sell": [...]}
GET /api/v1/cmc/summary
GET /api/v1/cmc/assets
GET /api/v1/cmc/ticker
GET /api/v1/cmc/orderbook/CURE_GRC?depth=0&level=2
GET /api/v1/cmc/trades/CURE_GRC
Orderbook depth as for CoinGecko (default 0 = full book, at most 500 levels per side); level 1 = best bid and ask only, 2 = aggregated by price (default), 3 is answered like 2 (individual orders are not published). Trades: the 500 most recent, timestamp in Unix milliseconds. unified_cryptoasset_id, base_id and quote_id are CoinMarketCap ids (CURE 333, GRC 833).
[{"trading_pairs": "CURE_GRC", "base_currency": "CURE", "quote_currency": "GRC",
"last_price": "2.50000000", "lowest_ask": "2.60000000", "highest_bid": "2.00000000",
"base_volume": "3.50000000", "quote_volume": "8.50000000", "price_change_percent_24h": "25.00000000",
"highest_price_24h": "3.00000000", "lowest_price_24h": "2.00000000"}]
{"CURE_GRC": {"base_id": 333, "quote_id": 833, "last_price": "2.50000000",
"base_volume": "3.50000000", "quote_volume": "8.50000000", "isFrozen": 0}}
{"timestamp": 1790683200250, "bids": [["2.00000000", "4.00000000"]], "asks": [["2.60000000", "4.00000000"]]}
Estimated live compute of distributed science networks, as shown in the ticker on the Markets page. BOINC figures are estimated from credit (200 credits/day = 1 GFLOPS); Folding@home is as reported. No authentication. See the methodology.
Cache-Control: public, max-age=300.Access-Control-Allow-Origin: *)."ok": false, "flops": null and a note with the reason. Values are never filled in."scope": "network" is the whole project; "scope": "gridcoin" means the project's own figure could not be read and the value is its Gridcoin crunchers' share from the latest Gridcoin superblock (CSV source: gridcoin_superblock).GET /api/public/science-compute
Every network with its current FLOPS, 24-hour change and 24 hourly sparkline values (oldest first, nulls allowed), plus the totals.
{"updated_at": "2026-10-09T19:00:00Z", "total_estimated_flops": 1.69e16, "gridcoin_whitelist_flops": 1.32e16,
"total_human": "16.9 PFLOPS", "gridcoin_whitelist_human": "13.2 PFLOPS", "formula": "...",
"networks": [{"id": "milkyway", "name": "MilkyWay@home", "short": "MW", "type": "boinc",
"flops": 1.35e14, "flops_human": "135 TFLOPS", "kind": "estimated", "change_24h_pct": 2.4,
"sparkline": [null, 1.33e14, 1.34e14], "source_url": "https://milkyway.cs.rpi.edu/milkyway/server_status.php?xml=1",
"sampled_at": "2026-10-09T19:00:00Z", "ok": true, "note": null, "gridcoin_whitelisted": true, "scope": "network"}],
"methodology_url": "/static/science-compute.html"}
GET /api/public/science-compute?format=csv
Every stored sample of the last 30 days: sampled_at, network_id, flops_estimate, credit_per_day, source, ok, source_url.
Create keys in Settings → API Keys. Choose permissions independently: Read (balances, orders, trade history, withdrawals), Trade (place and cancel orders) and Withdraw (send coins to your whitelisted addresses; see below). New keys are read-only by default, and Trade never includes Withdraw. The secret is shown only once. Optionally restrict a key to specific IP addresses. Never share the secret, paste it into a support ticket, or commit it to code; if it leaks, delete the key and create a new one.
X-API-Key: hx_your_key_id X-API-Timestamp: 1790452378 X-API-Nonce: 3f9c1d7e8b2a4c6d9e0f1a2b3c4d5e6f X-API-Signature: 5b1f...hex... User-Agent: my-bot/1.0 (every request, public ones too) Content-Type: application/json (for requests with a body)
my-bot/1.0, on every request (public endpoints included). Default library user agents such as Python-urllib/3.x may be blocked by the site's CDN with HTTP 403 before the request reaches HeliEx. It is not part of the signature.A-Z a-z 0-9 . _ ~ -, unique for every request (a random 32-hex-character value is ideal). A missing nonce returns HTTP 401; reusing one returns HTTP 409 "API request already used".request_target = path + ("?" + raw_query_string if there is a query)
message = timestamp + nonce + METHOD + request_target + body
signature = hex( HMAC_SHA256(key = secret, msg = message) )
METHOD is uppercase. The query string must be byte-for-byte what you send. The body must be exactly the bytes you send (serialize JSON once, sign that string, send that string); for GET it is empty.
GET /api/orders?status=open,partial
message = "1790452378" + "3f9c1d7e8b2a4c6d9e0f1a2b3c4d5e6f" + "GET" + "/api/orders?status=open,partial"
POST /api/orders body = {"side":"buy","price":"2.1","amount":"5","client_order_id":"bot-000123"}
message = "1790452378" + "<nonce>" + "POST" + "/api/orders" + body
GET /api/balances
Permission: Read.
[
{"asset": "GRC", "available": "100.00000000", "locked": "10.00000000"},
{"asset": "CURE", "available": "50.00000000", "locked": "0.00000000"}
]
GET /api/orders?limit=200&offset=0
GET /api/orders?status=open,partial
GET /api/orders?client_order_id=bot-000123
Permission: Read. Newest first; limit 1-200 (default 200), offset for paging. Optional filters: status (comma-separated: open, partial, filled, cancelled - open,partial lists every live order) and client_order_id.
[
{
"id": 42,
"side": "buy",
"price": "2.10000000",
"amount": "10.00000000",
"remaining": "6.00000000",
"filled": "4.00000000",
"status": "partial",
"created_at": "2026-09-26T18:38:11.552Z",
"client_order_id": "bot-000123"
}
]
remaining to 0.GET /api/orders/{order_id}
Permission: Read. Same format. HTTP 404 if it does not exist or is not yours.
There is no "rejected" state: an invalid order returns an HTTP error and is never created.
POST /api/orders
Permission: Trade.
{
"side": "buy",
"price": "2.1",
"amount": "5",
"client_order_id": "bot-000123",
"confirm_price_deviation": false
}
A-Z a-z 0-9 - _ . :), unique per account. Resending the same key with the same side, price and amount returns the original order (HTTP 200) instead of placing a second one - use this to retry safely after a timeout. The same key with different terms returns HTTP 409 client_order_id_conflict with the original order.All orders are limit orders: whatever does not fill immediately rests on the book at your price. Returns HTTP 200 with the order (format above) after any immediate fills. Funds are locked while the order is live: a buy locks price × amount GRC, a sell locks amount CURE.
Price protection: an order priced more than 10% worse than the market - buying above the best ask, or selling below the best bid (the last trade if that side is empty) - returns HTTP 400 with "code": "price_deviation_confirm_required", reference_price and deviation_percent. Resend with "confirm_price_deviation": true only if intended. Orders more than 50% worse are always rejected with "code": "price_deviation_blocked". Resting orders on the passive side (e.g. a low bid) are never affected.
Trading limits: order total (price × amount) at least 0.01 GRC; at most 100 live orders, 5,000 new orders per UTC day and 30 order placements per minute per account. An order that would trade against your own resting order is rejected (HTTP 400) - cancel that order first.
Fees: the trading fee is deducted from the seller's GRC proceeds; buyers pay no trading fee. The exact fee of every fill is in /api/my_trades.
POST /api/orders/{order_id}/cancel
Permission: Trade. No body. Unfilled funds are unlocked immediately.
{"detail": "Order cancelled", "order": {...}} - the final order, including filled.{"detail": "Order not cancellable: already filled", "status": "filled" | "cancelled", "order": {...}}POST /api/orders/cancel_all
POST /api/orders/cancel_all?side=buy
Permission: Trade. No body. Cancels every live order (open or partial), or only one side. Safe to repeat.
{"detail": "Cancelled 2 orders", "cancelled": 2, "orders": [{...}, {...}]}
GET /api/my_trades?limit=100&offset=0
Permission: Read. Your individual fills, newest first (limit 1-500, default 100). price is the actual execution price; fee is what you paid on that fill (0 on buys).
[
{
"id": 9001,
"side": "sell",
"order_id": 42,
"price": "2.00000000",
"amount": "4.00000000",
"fee": "0.01600000",
"fee_asset": "GRC",
"created_at": "2026-09-26T18:38:11.552Z"
}
]
Withdrawals made through the API use the same pipeline as website withdrawals: the same fees, balance debit, chain-safety checks and sending. Extra safeguards apply because a leaked key could otherwise move funds.
X-Forwarded-For and similar headers are ignored).POST /api/key/withdrawals
Permission: Withdraw (a session login cannot use this endpoint). Body fields: asset (GRC or CURE), amount (decimal string, up to 8 decimals; the fee is added on top), address (a usable whitelisted address), client_id (your unique id, 1-64 characters A-Z a-z 0-9 . _ : -). Limit: 10 requests per minute per key.
POST /api/key/withdrawals
{"asset":"GRC","amount":"25.5","address":"SAbc...xyz","client_id":"payout-2026-10-01-001"}
201 Created
{
"id": 1532,
"client_id": "payout-2026-10-01-001",
"asset": "GRC",
"amount": "25.50000000",
"fee": "0.60000000",
"address": "SAbc...xyz",
"status": "pending", ("held" when the key has a hold)
"txid": null,
"release_at": null, (when a held withdrawal will be sent)
"created_at": "2026-10-01T20:15:03.120Z"
}
Idempotent: sending the same client_id again with the same asset, amount and address returns the original withdrawal (HTTP 200), never a second one. The same client_id with different details returns 409 CLIENT_ID_CONFLICT. After a timeout, resend with the same client_id and a new nonce.
GET /api/key/withdrawals?limit=50&before_id=1532&status=held
GET /api/key/withdrawals/1532
Permission: Read. Lists all of your withdrawals (website and API), newest first; limit 1-100, page with before_id = the returned next_before_id. Optional status filter.
{"withdrawals": [{...}, {...}], "next_before_id": 1490}
release_at; can still be cancelled.txid set).Refusals change nothing (no debit) and return {"error": "CODE", "detail": "message", ...}.
usable_after says when it ends.usable_after), ADDRESS_REMOVED - whitelist rules.limit, used and remaining (amount + fee, rolling 24 h).required = amount + fee).client_id already used for a different withdrawal. 409 REPLAYED_REQUEST - nonce reused.Retry-After.403 Forbidden
{"error": "DAILY_LIMIT_EXCEEDED", "detail": "This key's daily withdrawal limit would be exceeded",
"limit": "100.00000000", "used": "92.40000000", "remaining": "7.60000000"}
The swap page asks people for a human check. A program can skip it by paying a small fee on-chain from its own wallet first. No account, login or API key is needed, so your coins stay in your own wallet. Everything after the fee is a normal public swap (see the Swap Terms): 1-hour funding window, 12 GRC / 6 CURE confirmations, your minimum, automatic returns, recovery link.
POST /api/swaps/fee-request
No authentication. Body: send_asset (GRC or CURE), amount (decimal string), receive_address, return_address, and optionally minimum (exact coins) or tolerance_percent.
201 Created
{"request_id": "4bcf0be1...", "lane": "fee-first-v1", "status": "awaiting_payment",
"expires_at": "2026-10-02T21:50:35Z",
"fee": {"asset": "GRC", "address": "S_FEE_ADDRESS", "amount": "1.00009049", "confirmations_required": 12,
"refundable": false},
"swap": {"send_asset": "GRC", "amount": "10", "receive_asset": "CURE", "indicative_estimate": "2.21222200",
"minimum_receive": "2.10161090", "minimum_mode": "percentage"}}
fee.amount in one payment to fee.address before expires_at (1 hour). Nothing is reserved before the fee confirms.GET /api/swaps/fee-request/{request_id} about once a minute. Keep the id private: it returns your recovery token. status: awaiting_payment → payment_seen → creating → swap_created (then swap holds the deposit address, exact amount, expiry and recovery link), or refused / fee_returned (fee returned, see detail) or expired.swap.deposit.address (another 12 GRC / 6 CURE confirmations). Total time is typically 40-60 minutes.Another amount, a second payment, or a payment after expiry is returned to your return_address minus the 0.01 network cost once confirmed. If the swap cannot be created within 30 minutes of the fee confirming (price below your minimum, limits, pause), or is held before any deposit address was issued, the fee is returned the same way. Payments we cannot match go to operator review.
Retry-After.import time, requests
API = "https://heliex.net"
UA = {"User-Agent": "my-bot/1.0"}
req = requests.post(API + "/api/swaps/fee-request", headers=UA, timeout=60, json=dict(
send_asset="CURE", amount="2.5", receive_address="YOUR_GRC_ADDRESS", return_address="YOUR_CURE_ADDRESS",
tolerance_percent="2")).json()
print("pay exactly", req["fee"]["amount"], req["fee"]["asset"], "to", req["fee"]["address"], "by", req["expires_at"])
# ... send the fee from your own wallet, then:
while True:
state = requests.get(API + "/api/swaps/fee-request/" + req["request_id"], headers=UA, timeout=60).json()
if state["status"] in ("swap_created", "refused", "fee_returned", "expired"):
break
time.sleep(60)
if state["status"] == "swap_created":
swap = state["swap"]
print("send", swap["deposit"]["amount"], "to", swap["deposit"]["address"], "recovery:", swap["recovery"]["url"])
Every private response includes:
X-RateLimit-Limit: 60 requests allowed per minute for this kind of request X-RateLimit-Remaining: 57 requests left in the current window X-RateLimit-Reset: 42 seconds until the oldest counted request expires
When a limit is exceeded: HTTP 429 with a Retry-After header (seconds). Wait that long before retrying. The per-account order limits (30 per minute, 5,000 per UTC day) also send Retry-After.
Errors return JSON with a detail message; some also include a machine-readable code. The withdrawal endpoint uses an error code instead (see API Withdrawals).
code field), invalid filter.User-Agent such as my-bot/1.0.client_order_id_conflict.detail lists the problems.Retry-After. Also returned when you already have 100 live orders - cancel some first.{"detail": "Maintenance"}) or a wallet is offline. Retry later; check /api/wallet_status.A network error or timeout does not mean an order failed. Before retrying, look it up by client_order_id - or simply resend it with the same client_order_id.
import hashlib, hmac, json, secrets, time
from decimal import Decimal
import requests
BASE_URL = "https://heliex.net"
KEY_ID = "hx_your_key_id"
SECRET = "your_secret" # load from an environment variable, never commit it
USER_AGENT = "my-bot/1.0" # descriptive; default library agents may get HTTP 403
def api(method, path, data=None):
body = json.dumps(data, separators=(",", ":")) if data is not None else ""
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16) # new for every request
message = timestamp + nonce + method + path + body
signature = hmac.new(SECRET.encode(), message.encode(), hashlib.sha256).hexdigest()
headers = {"X-API-Key": KEY_ID, "X-API-Timestamp": timestamp, "X-API-Nonce": nonce,
"X-API-Signature": signature, "Content-Type": "application/json",
"User-Agent": USER_AGENT}
r = requests.request(method, BASE_URL + path, headers=headers, data=body or None, timeout=15)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
return api(method, path, data) # re-signed with a new nonce
r.raise_for_status()
return r.json()
book = requests.get(BASE_URL + "/api/orderbook", headers={"User-Agent": USER_AGENT}, timeout=15).json()
best_ask = Decimal(book["asks"][0]["price"]) if book["asks"] else None
order = {"side": "buy", "price": "2.1", "amount": "5", "client_order_id": "bot-000123"}
try:
placed = api("POST", "/api/orders", order)
except (requests.ConnectionError, requests.Timeout):
placed = api("POST", "/api/orders", order) # same client_order_id: no duplicate
print(placed["status"], Decimal(placed["filled"]))
live = api("GET", "/api/orders?status=open,partial")
api("POST", "/api/orders/cancel_all")
sequence and /api/trades?since_id to detect changes.