native-core-python-sdk
A Python SDK for trading on Native Core — read market data, place and cancel orders, and run trading bots against the public gateway.
Mainnet moves real funds. Develop against testnet first, use a scoped API wallet (never your main wallet), and start with small sizes. Mainnet is
api.native.org(chain 696969), testnet isapi-test.native.org(chain 969696);from_bundlepicks the gateway from the bundle'snetwork.
Info handles reads (POST /info), Exchange handles writes (POST /trade), and WsClient streams live data over a WebSocket. Everything is synchronous (built on requests and websocket-client) and returns the gateway's JSON as plain dicts, annotated with TypedDict; the stream delivers frames from a background thread, so no part of the SDK asks you to write async code. Spot order-book markets only — no perpetuals. A /trade write returns its settled outcome inline, so you never poll to learn whether it landed. Deposits and withdrawals stay in the web app. Requires Python 3.10+ (requests, eth-account, eth-utils, websocket-client).
Rate limits are tighter than most people expect. On mainnet an address gets one order per second and one read per second, and that budget is shared between HTTP and the WebSocket. See §7 Rate limits.
1. Install
pip install native-core-python-sdk
Pin a version for reproducible installs: pip install native-core-python-sdk==1.1.0.
2. Get an API wallet
You need a funded account and an API wallet (your bot's key), set up once in the app — the SDK never handles funds. In the Native app (mainnet or testnet): connect your main wallet, deposit to fund it (testnet has no faucet — bring Arbitrum Sepolia; mainnet takes real assets), then Create API wallet. You sign one approval and the app shows the connection bundle once — copy it.
Save it (e.g. bundle.json, what the quickstart loads) and pass it to the SDK as a file path, dict, or JSON string:
{
"network": "testnet", // "mainnet" or "testnet" — picks the gateway
"accountAddress": "0x…", // your main wallet, the account the bot trades on
"agentPrivateKey": "0x…" // the API wallet key the SDK signs with
}
A leaked API wallet key can trade your balance but can never withdraw or move funds off Native, so it is safe in a bot; revoke it in the app to rotate.
3. Quickstart
Place a resting limit order, confirm it rests, then cancel it. Point BUNDLE at a testnet bundle to try it risk-free:
from decimal import Decimal
from native_core import Exchange, is_accepted, order_state
BUNDLE = "bundle.json" # the file you saved, or the dict / JSON string itself
MARKET = "ETH/USDT"
exchange = Exchange.from_bundle(BUNDLE) # picks the gateway, loads the key, sets owner
info = exchange.info
# Check the API wallet is approved before you trade.
print(exchange.agent_info()) # {"approved": True, "slot_id": 0, "epoch": 10}
# Price a bid well below the market so it rests instead of filling.
book = info.l2_book(MARKET, depth=1)
reference = book["asks"] or book["bids"] # whichever side has liquidity
px = info.snap_price(MARKET, Decimal(reference[0]["price"]) / 2)
sz = info.min_order_size(MARKET, px)
# place() submits the order and waits for it to rest, returning the handle and state.
order = exchange.place(MARKET, is_buy=True, sz=sz, limit_px=px, tif="gtc")
print(order["cloid"], order["state"]) # 0x… open
# Cancel it, then confirm it left the book.
cancel = exchange.cancel_by_cloid(MARKET, order["cloid"])
assert is_accepted(cancel)
final = info.wait_for_order(exchange.effective_account, MARKET, order["cloid"])
print(order_state(final)) # cancelled
Two ideas this leans on — a write returns its settled outcome directly, and every order carries a reconcilable cloid — are the heart of the next section.
4. Core concepts
Reads vs writes. Info wraps POST /info (market data, balances, order status); Exchange wraps POST /trade (orders). Both return the gateway's raw JSON.
The write returns the settled outcome. A write's submission_status is exactly one of three values: accepted (the tx landed and executed — this INCLUDES a benign no-fill/no-rest cancel like ioccancel or marketordernoliquidity), rejected (an admission reject or a genuine execution failure, reason on error.code), or timeout (indeterminate). You never poll to learn whether the write landed. What the response does not carry is the oid, the fill amount, or the fill/rest/cancel lifecycle — those live only on /info order status, so read the order once when you need them:
wait_for_open(user, market, cloid)— for an order you expect to rest (gtc / alo); returns the resting snapshot (with theoid).wait_for_order(user, market, cloid)— for one you expect to finish (ioc / fok / market, or after a cancel); returns the terminal snapshot (withfilled_qty).exchange.place(...)does that one enrichment read for you, returning{cloid, submission, status, state}.
Do not call wait_for_order on a resting order — it has no terminal state, so the call just times out. That is what wait_for_open is for.
Every order is reconcilable. order() and market_order() return the cloid and nonce they used; batch() returns cloids (one per leg) plus the shared nonce. Pass your own cloid or let the SDK generate one — either way the response carries it. If a write times out on the wire, the SDK raises SubmissionUncertain with the cloid attached; reconcile with wait_for_order, and never resubmit under a fresh nonce or the order may land twice.
One Exchange per API wallet. The nonce is a per-instance, lock-guarded monotonic counter, so a single Exchange is safe to share across threads. Two instances — or two processes — signing with the same key hand out colliding nonces and draw seemingly random rejections. Construct one and share it.
Numbers are strings, never floats. Pass sz and limit_px as str or Decimal ("0.01", not 0.01). Each market has a fixed precision; info.snap_price(market, price) and info.min_order_size(market, price) round a value to what it accepts. The SDK validates before signing and raises LocalValidationError rather than silently round your number down.
Errors live in the response body. A business rejection is data, not an exception: {"submission_status": "rejected", "error": {"code": …}}. Read it with is_accepted, is_rejected, is_timeout, error_code, retry_after_ms, is_retryable. The SDK raises only for a transport failure (NetworkError), a non-trade response (ClientError / ServerError), or a pre-sign problem (LocalValidationError).
Markets by symbol or id. Anywhere a market argument appears, pass "ETH/USDT" or its integer id. Symbols and precision are fetched once when you construct Info.
Networks. An API wallet is bound to one network: the chain id is part of every signed payload, so signing with a key from the wrong network is rejected with WrongChainId. from_bundle matches the gateway to the bundle's network for you.
5. Running the examples
The examples ship in the source distribution (the .tar.gz on PyPI), under examples/. They read examples/config.json — copy the template and fill in your key:
cp examples/config.json.example examples/config.json
{ "secret_key": "0x<agentPrivateKey>", "account_address": "0x<accountAddress>" }
This is the connection bundle flattened: secret_key is the bundle's agentPrivateKey, account_address its accountAddress. config.json has no network field — the examples hardcode constants.TESTNET_API_URL, so they always run on testnet; edit that argument to constants.MAINNET_API_URL only to trade real funds. A blank or malformed key fails with a clear LocalValidationError.
# read-only tours (examples/info/) — place no orders, but still need a valid
# secret_key in config.json (account_address is derived from it when blank)
python examples/info/query_markets_info.py # tradable markets and their precision
python examples/info/query_orderbook_info.py # the L2 order book for a market
python examples/info/query_balances_info.py # your spot balances
python examples/info/query_open_order_info.py # your open orders in a market
# trading tours — place real orders on testnet, ETH/USDT (edit MARKET to change)
python examples/basic_order.py # resting limit order: place, confirm, cancel
python examples/basic_market_order.py # market order with a protection price
python examples/basic_batch.py # several orders and cancels under one nonce
# live data — needs no key and places no orders, so it runs as-is
python examples/ws_feeds.py # stream the book, top of book, and trades
python examples/ws_feeds.py https://api.native.org # ... against mainnet
6. Using it from an AI agent
The SDK is built to be driven by an AI agent, not just a person: it returns each outcome as structured fields, so the agent branches on a field instead of parsing prose. One safety contract, two integration paths.
The safety contract
- Read the settled status; a fill is not implied.
acceptedmeans the tx landed, but it INCLUDES a benign no-fill/no-rest cancel — it does not mean the order rested or filled. The response carries nooid, fill, or lifecycle: callreconcile_by_cloid(orwait_for_openfor a resting order) once to read them. - Never resubmit an uncertain write. A wire timeout (
SubmissionUncertain, orsubmission_status: "timeout") means the order MAY be live. Reconcile bycloid; resubmitting under a fresh nonce risks a double-fill. The response says so:next_actionisRECONCILE_BY_CLOID. - Rejections are data. A business rejection comes back with an
error.code; fix the cause. OnlyRateLimitedis safe to resend (is_retryable). - Numbers are strings. Size with
min_order_sizeand validate withbuild_order(a dry run that sends nothing) to avoid precision / minimum-notional rejections. - Survive a restart. Generate the
cloidyourself withrandom_cloid(), persist{intent, cloid}durably before callingorder(..., cloid=cloid), and on restart reconcile every persisted cloid before placing anything new. An agent that crashes after sending but before recording an SDK-generated cloid cannot reconcile and may double-fill — this is the idempotency-key pattern.
next_action(response) collapses any trade response into one verdict (None for a non-trade response):
next_action |
Situation | What the agent does |
|---|---|---|
READ_ORDER_STATUS |
accepted — landed & executed |
Read order_status once for the oid / fill (already settled — a single read, not a poll) |
RECONCILE_BY_CLOID |
timeout — indeterminate |
reconcile_by_cloid; never resubmit |
BACKOFF_AND_RETRY |
RateLimited — never admitted |
Sleep retry_after_ms, then resend the same order |
FIX_AND_RESUBMIT |
rejected — admission or execution failure |
Read error.code, fix the input or account state, submit fresh |
as_problem_details(failure) renders any exception or rejected/timeout body into one flat {type, title, retryable, next_action, cloids, …} envelope.
Option A — the MCP server (no glue code)
An optional MCP server lets an AI assistant that speaks the Model Context Protocol (such as Claude Desktop) read the market and place orders through the SDK. Install the extra:
pip install "native-core-python-sdk[mcp]"
Configure it from the environment (never from the assistant). Reads are always available; the write tools are registered only when you turn trading on, so a read-only setup physically cannot place an order. If NATIVE_CORE_ENABLE_TRADING=1 but the agent key is missing, malformed, or unapproved, the server starts read-only and prints the reason instead of crashing.
| Variable | Purpose |
|---|---|
NATIVE_CORE_BUNDLE |
Path to a connection bundle JSON. Supplies everything on its own. |
NATIVE_CORE_NETWORK |
testnet (default) or mainnet, when not using a bundle. |
NATIVE_CORE_ACCOUNT |
Your account address, for account-scoped reads. |
NATIVE_CORE_AGENT_KEY |
The API wallet's private key, required for trading. |
NATIVE_CORE_ENABLE_TRADING |
Set to 1 to register the write tools. Off by default. |
Run it over stdio with native-core-mcp. To connect it to Claude Desktop, add to its MCP config:
{
"mcpServers": {
"native-core": {
"command": "native-core-mcp",
"env": {
"NATIVE_CORE_BUNDLE": "/path/to/your/bundle.json",
"NATIVE_CORE_ENABLE_TRADING": "1"
}
}
}
}
Read tools: whoami, list_markets, get_orderbook, get_balances, list_open_orders, get_order, get_fills, get_min_order_size, reconcile_order. Write tools (trading only): preview_order, place_limit_order, place_market_order, cancel_order, cancel_all_orders. Every result is normalized so the assistant sees the order's state and what to do next; a timed-out order comes back telling it to reconcile by cloid, never to resubmit. Use a dedicated, revocable API wallet — these tools are convenience, not a security boundary.
Option B — drive the SDK from your agent code
If your agent does its own tool-calling, call the SDK directly and hand the model the same fields:
from native_core import Exchange
exchange = Exchange.from_bundle(BUNDLE)
order = exchange.place(MARKET, is_buy=True, sz=sz, limit_px=px, tif="gtc")
# Resolve the real outcome by cloid — never resubmit on an uncertain one.
verdict = exchange.info.reconcile_by_cloid(exchange.effective_account, MARKET, order["cloid"])
if verdict["undetermined"]:
... # not confirmed yet: keep reconciling, do not re-place
elif verdict["is_filled"]:
... # fully filled
elif verdict["filled_qty"] != "0":
... # partially filled and still resting
else:
... # resting, unfilled (verdict["state"], e.g. "open")
The MCP tools are thin wrappers over these calls: get_order ≈ reconcile_by_cloid, get_min_order_size ≈ min_order_size, preview_order ≈ build_order.
7. API reference
Info (reads)
Build with Info(base_url) or Info.from_bundle(bundle). Any market argument takes a "BASE/QUOTE" symbol or an integer market id.
| Method | Returns |
|---|---|
markets() / assets() / quote_assets() |
Tradable markets and their assets, with precision |
resolve_market_id(market) |
Resolve a "BASE/QUOTE" symbol to its integer market id |
l2_book(market, depth=20) |
Order book, up to 100 levels |
mark_prices(asset_ids=None) / oracle_status() |
Mark prices; oracle health |
query_status() |
Current query height and the available block-height window |
user_balances(address) |
Spot balances |
open_orders(address, market) |
Resting orders in one market |
open_orders_all(address, markets=None) |
Open orders across markets, each tagged with its market id |
order_status(oid=None, user=None, market=None, cloid=None) |
One order, by oid or by (user, market, cloid) |
user_fills(address, from_height, to_height, limit) |
Fills in a raw block-height window (≤10,000 blocks) |
recent_fills(address, blocks=10000) |
Every fill in roughly the last N blocks, window resolved for you |
iter_user_fills(address, start_height=None, …) |
Iterate all fills since a height, paged and deduped |
tx_status(user, cloid) |
Status of a deposit / withdraw / settle / repay by cloid |
account_status(address) |
Whether an account exists and its state |
spot_credit_account(address) / spot_credit_positions(address) |
Credit-account details |
user_agents(address) / agent_status(owner, agent_address) |
Approved API wallets; whether one is approved |
snap_price(market, price) / min_order_size(market, price) |
Round a price or size to what the market accepts |
protection_price(market, is_buy, slippage_bps, ref_price=None) |
Worst acceptable price for a market order, derived from the book |
wait_for_open(user, market, cloid, timeout=5.0) |
Poll until the order is resting or terminal |
wait_for_order(user, market, cloid, timeout=5.0) |
Poll until the order is terminal |
reconcile_by_cloid(user, market, cloid, timeout=5.0) |
Did an order land? A one-call verdict (state, undetermined, is_filled, filled_qty, plus the raw status) |
Exchange (writes)
Build with Exchange.from_bundle(bundle), or Exchange(wallet, base_url, owner=<account_address>) where wallet is an eth_account account from your API wallet key.
| Method | Description |
|---|---|
place(market, is_buy, sz, limit_px, tif, cloid=None, *, confirm=True, timeout=5.0) |
Submit an order and wait for the outcome that matches its tif; returns {cloid, submission, status, state} |
order(market, is_buy, sz, limit_px, tif, cloid=None) |
Limit order. tif is gtc / ioc / fok / alo |
market_order(market, is_buy, sz, protection_px=None, tif="ioc", cloid=None, *, slippage_bps=None) |
Market order (ioc / fok). Pass protection_px or slippage_bps to derive it from the book |
build_order(market, is_buy, sz, limit_px, tif, cloid=None, order_type="limit") |
Dry run: validate and build an order without signing or sending it |
cancel(market, oid) / cancel_by_cloid(market, cloid) |
Cancel one order |
cancel_all(market) |
Cancel every open order in a market |
cancel_open(markets=None) |
Cancel every open order across markets (only where orders rest) |
modify(market, oid_or_cloid, replacement) |
Atomically cancel and replace one order |
batch(items) |
Up to 10 mixed order / cancel / cancelAll / modify actions under one nonce |
set_expires_after(expires_after_ms) |
Attach an expiry to every signed action |
agent_info() |
Whether this API wallet is approved on the owner: {approved, slot_id, epoch} |
Each write returns the raw gateway response with the client handle echoed in: submission_status, tx_hash, error, cloid (or cloids for a batch), and nonce. Also on Exchange: the agent_address property (the signing wallet), effective_account (the account orders act on: the owner in agent mode, else the wallet), and the static Exchange.random_cloid().
WsClient (live data)
Build with WsClient(base_url), or Info.ws() / Exchange.ws() to reuse a market table already loaded. Call connect() before subscribing, and close() when done (it is also a context manager). Nine feeds stream over one connection, and examples/ws_feeds.py is a runnable tour that needs no key.
ws = WsClient("https://api.native.org")
ws.connect()
ws.subscribe_l2_book("ETH/USDT", lambda data: print(data["levels"])) # callback style
ws.subscribe_trades("ETH/USDT") # iterator style
for message in ws.stream():
print(message["channel"], message["data"])
| Method | Feed |
|---|---|
subscribe_trades(market, cb=None) |
Every trade printed on a market |
subscribe_l2_book(market, cb=None) |
Full book snapshot, ≤10 levels a side, ≤2/s |
subscribe_bbo(market, cb=None) |
Top of book, only when it changes |
subscribe_all_mids(cb=None) |
Mid price for every market, ≤1 per 5s |
subscribe_user_fills(address, cb=None) |
An account's fills; first message replays the recent 100 |
subscribe_order_updates(address, cb=None) |
Order lifecycle: open / filled / canceled |
subscribe_open_orders(address, cb=None) |
All resting orders, as a full replacement |
subscribe_spot_state(address, cb=None) |
Spot balances, as a full replacement |
subscribe_spot_credit_state(address, cb=None) |
Credit positions and credit line |
subscribe(body, cb=None) / unsubscribe(sub) |
Raw subscription body; stop a feed |
stream(timeout=None) |
Iterate feeds subscribed without a callback |
post_info(payload) / post_action(signed_body) |
Run an /info read or submit a signed order over the socket |
A callback receives the payload and runs on the reading thread, so it must return quickly — the server disconnects a client that stops draining. Subscribing without one routes the feed to stream(), which buffers (queue_maxsize, default 10,000, dropping oldest and counting them in dropped_messages).
The socket names the same data differently than HTTP does, and one pair is a trap. These are the gateway's Hyperliquid-compatible names; the SDK passes them through rather than inventing a third vocabulary. Check this table before swapping a polling loop for a feed:
HTTP (Info) |
socket (WsClient) |
|
|---|---|---|
| Book level | price, quantity, order_count |
px, sz, n |
| Balance | asset_id, symbol, available, locked |
token, coin, total, hold |
available and total are not the same quantity. available is what you can spend; total is everything you hold, available + locked. Reading total where you used to read available overstates your free balance by exactly the amount sitting in resting orders — silently, and only once you have orders on the book. Use total - hold:
# Info.user_balances # spotState feed
free = Decimal(row["available"]) free = Decimal(row["total"]) - Decimal(row["hold"])
Subscribing blocks until the server answers, so a refusal raises SubscriptionError at the call rather than surfacing later. Reconnects and resubscribes are automatic; on_reconnect fires once subscriptions are restored. Snapshot feeds repair themselves and userFills replays on resubscribe, but trades and orderUpdates gap across a disconnect — re-read those through Info from on_reconnect. Note that orderUpdates frames carry no address, so subscribing several accounts on one connection delivers every account's updates to every callback; tell them apart by oid.
To submit an order over the socket, Exchange.build_order validates and builds it, Exchange.sign_action signs it (consuming a nonce, so the body is single-use), and post_action sends it:
built = exchange.build_order("ETH/USDT", True, "0.01", "1900", "gtc")
response = ws.post_action(exchange.sign_action(built["action"]))
A write reports less here than over HTTP. When the gateway answers a write with anything other than HTTP 2xx, the server renders the reply as a bare string and discards the trade response — so submission_status, tx_hash and retry_after_ms are gone on exactly the paths where you would want them. post_action turns those into exceptions instead: a refusal before the node (400 bad signature or nonce, 429 per-signer rate limit) raises ClientError, and everything else — including the routing failures HTTP reports as a timeout (HandoffTimeout, HandoffBufferFull, node_unreachable) — raises SubmissionUncertain, because the order may still have landed. Reconcile those by cloid; never resubmit under a fresh nonce. A write is also never retried automatically, including on a rate limit.
The socket caps a message at 64 KiB where HTTP /trade accepts 256 KiB, so an oversized request is refused locally rather than silently closing the connection.
None of this is faster — it shares one rate budget with HTTP and measures the same round trip (mainnet: 145ms over HTTP, 146ms over the socket). Prefer Exchange.order; the socket write path earns its keep only at request rates the default limits do not allow.
Response helpers
is_accepted, is_rejected, is_timeout, error_code, retry_after_ms, is_retryable classify a trade response, and next_action turns one into a single verdict. order_state pulls the status out of an order_status response; is_terminal / is_undetermined classify it, and is_filled / filled_quantity read the fill. as_problem_details renders any failure into one flat envelope; retry_on_rate_limit(action) wraps a write to resend only on RateLimited. random_cloid returns a fresh client order id.
Exceptions
Everything inherits from native_core.Error. Business rejections are not exceptions; they arrive in the response body (above).
| Exception | Raised when |
|---|---|
LocalValidationError |
Before signing: bad precision, below minimum notional, unknown tif/market, or (at construction) an API wallet that is not an approved agent |
NetworkError |
A transport failure (timeout, connection, DNS) before any response arrived |
SubmissionUncertain |
A write was signed and sent, then timed out. Carries cloid and nonce to reconcile |
ClientError |
An HTTP 4xx whose body is not a trade response. Has status_code, error_code, error_message |
ServerError |
An HTTP 5xx whose body is not a trade response. Has status_code |
SubscriptionError |
A WebSocket subscribe or unsubscribe was refused or went unanswered. Has reason and subscription |
The WebSocket raises the same exceptions the HTTP path does, so one except ClientError covers both transports: the server renders a post-channel failure as the status the equivalent HTTP call would have returned, and an unanswered signed order raises SubmissionUncertain carrying its cloids exactly as a timed-out POST /trade does.
ErrorCode is a convenience enum of known codes, all CamelCase: RateLimited, ExpiredTx, WrongChainId, DirectSignerIsActiveAgent, AgentEpochMismatch, InsufficientSpotBalance. Execution-stage failures arrive as raw lowercase strings that are not enum members — insufficientspotbalance, badalopx, tick, lotsize, marketclosed — and the set is open-ended, so error_code(resp) returns anything outside the enum as a raw string. One case to know: a post-only (alo) order that would cross comes back accepted (the tx succeeded) with badalopx on its orderStatus lifecycle — the order did not rest. The sharpest case of accepted ≠ rested: read orderStatus for the real outcome.
Rate limits and retries
The gateway can rate-limit requests: an HTTP 429 with error.code RateLimited and a suggested retry delay. Because a 429 never reached the node, the SDK retries it automatically — a bounded rate_limit_retries (default 3), backing off by the server's delay — so construction and read bursts stay resilient. Pass rate_limit_retries=0 to surface a 429 at once; when retries are exhausted a read raises ClientError.
Budget your bot against these numbers. Writes and reads have separate per-address budgets, and each is shared between HTTP and the WebSocket — sending a request through the socket buys no extra headroom. Mainnet, as deployed:
| Per address | |
|---|---|
| Orders per second | 1 |
| Reads per second | 1 |
| Subscriptions per connection | 10 |
| Requests outstanding at once | 1 |
| Connections per address | 1 (counted, not yet enforced) |
The platform sets these and may change them, so the SDK does not hard-code the per-second rates — it reads the delay out of each rejection. WsClient defaults max_subscriptions=10 and max_inflight_posts=1 to match; both are constructor arguments, so an integration that has been granted higher limits can raise them.
A RateLimited on a /trade write can instead arrive as a trade body (rejected with error.retry_after_ms); branch on it with is_retryable(resp) (true only for RateLimited, safe to resend) and back off with retry_after_ms(resp), or wrap the write in retry_on_rate_limit(...). A timeout is never retryable. Under load two other write-plane families appear: PlaceOrderSuspended (503, a clean rejected before the node when the chain is degraded and only cancels are admitted — back off with retry_after_ms and resubmit once it clears), and submit-routing failures (HandoffTimeout, HandoffBufferFull:*, HandoffMultipleActive at 503; node_unreachable at 504) that arrive as a timeout — indeterminate, so reconcile by cloid, never resubmit.
Transport controls
Info and Exchange (and their from_bundle) accept timeout (per-request deadline in seconds, default 30; None for no deadline), pool_maxsize (connection pool, default 100), rate_limit_retries (default 3; 0 to disable), and hooks:
on_request(url_path, body, trace_id)— before each request.on_response(url_path, status, body, elapsed_ms, trace_id)— after each;trace_idis thex-trace-idthe gateway returned, orNone.
The gateway accepts an x-trace-id request header and echoes it back, so you can line up a request with its logs. You supply the id — the SDK never generates one — via trace_id_factory (once per request) or trace_id= (per call to post).
import uuid
exchange = Exchange.from_bundle(
bundle,
trace_id_factory=lambda: str(uuid.uuid4()),
on_response=lambda path, status, body, ms, trace_id: log(path, trace_id),
)
Constants
constants.TESTNET_API_URL, constants.MAINNET_API_URL, and constants.NETWORK_URLS (network name → gateway URL). The chain id is derived from the URL — testnet 969696, mainnet 696969 — and is what binds a key to its network (see Networks in §4 for WrongChainId).
8. Troubleshooting
| You see | Meaning and fix |
|---|---|
LocalValidationError: … is not an active agent for owner (at construction) |
The API wallet is not approved on that account. Check the owner, or create a fresh one — the old key may have been revoked. info.agent_status(owner, agent) shows the current slots. |
SubmissionUncertain |
A write timed out on the wire. Reconcile with wait_for_order(user, market, e.cloid). Do not resubmit under a new nonce. |
NetworkError |
A transport failure on a read. Retry the read. |
DirectSignerIsActiveAgent |
You built Exchange with an API wallet key but no owner. Pass owner=<accountAddress>, or use from_bundle. |
WrongChainId |
The key's network does not match the gateway. from_bundle avoids this. |
LocalValidationError: … decimal places / significant figures |
sz or limit_px exceed the market's precision. Snap with info.snap_price / info.min_order_size. |
insufficientspotbalance (rejected) |
The account is not funded for that market's quote asset. |
submission_status: "rejected" |
A hard write failure — admission reject or execution failure. Read error_code(resp). Data, not an exception. |
submission_status: "timeout" |
Indeterminate: the order may still land. Reconcile with wait_for_order; never resubmit with a new nonce. |
RateLimited / HTTP 429 |
The gateway is throttling you. The SDK auto-retries with backoff (rate_limit_retries); if it still surfaces, slow down, raise rate_limit_retries, or wrap writes in retry_on_rate_limit. |
AgentEpochMismatch |
The SDK refreshes and retries once. If it persists, the API wallet was revoked or re-approved; create a new one. |
9. Development
make install # uv sync
make lint # ruff + mypy --strict
make test # pytest
Release files for native-core-python-sdk 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| native_core_python_sdk-1.1.0.tar.gz | 130.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| native_core_python_sdk-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 214.0 kB
Release files / native_core_python_sdk-1.1.0.tar.gz
| Download URL | native_core_python_sdk-1.1.0.tar.gz |
|---|---|
| Size | 130.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dc847c2004bc213f25b5c8498cb9a598d3caae8e2a4241bfb78c0b0c69a7dd0c
|
|
BLAKE2b-256 checksum How to use checksums |
bd4811716b7b6011dfd4d897e3b6c073c91eb364c926de57fd1823df091391ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","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 / native_core_python_sdk-1.1.0-py3-none-any.whl
| Download URL | native_core_python_sdk-1.1.0-py3-none-any.whl |
|---|---|
| Size | 83.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
22f269cfa57a9ee0f26a940cd589f6cbd19da3489dd75bea390942f5c934fa9c
|
|
BLAKE2b-256 checksum How to use checksums |
90f5d162795a03d43a9335441739f6b060d352d1e828466f6ca7c1f258f1e856
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","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}
|