<!-- https://zunderlabs.com/docs/integrations/python-sdk · Markdown version of the page -->

# Hyperliquid Python SDK

Put Guard in front of the official Hyperliquid Python SDK with base_url. The exact arguments.

:::note[Planned]
Guard 1.0 integration, not verified yet. Written from the official SDK's source, [`hyperliquid-dex/hyperliquid-python-sdk`](https://github.com/hyperliquid-dex/hyperliquid-python-sdk), files `hyperliquid/exchange.py`, `hyperliquid/utils/constants.py` and `hyperliquid/utils/signing.py` on `master`, read 6 Oct 2026. This is the one SDK Hyperliquid lists as official.
:::

## The change

`Exchange` takes a `base_url` argument (`Exchange.__init__(wallet, base_url=None, meta=None, vault_address=None, account_address=None, spot_meta=None, perp_dexs=None, timeout=None)`). It also builds its own `Info` client from the same URL.

```diff
-exchange = Exchange(api_wallet, constants.MAINNET_API_URL, account_address=ACCOUNT)
+exchange = Exchange(guard_client_key, "http://127.0.0.1:8547", account_address=ACCOUNT)
```

## Full example

```python
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info

GUARD = "http://127.0.0.1:8547"
ACCOUNT = "0xYourAccountAddress"               # your main account

client_key = Account.from_key("0x...")         # the client key Guard printed
info = Info(GUARD, skip_ws=True)
exchange = Exchange(client_key, GUARD, account_address=ACCOUNT)

# The entry and its stop in one request, grouping "normalTpsl".
entry = {"coin": "ETH", "is_buy": True, "sz": 2.0, "limit_px": 2500.0,
         "order_type": {"limit": {"tif": "Gtc"}}, "reduce_only": False}
stop = {"coin": "ETH", "is_buy": False, "sz": 2.0, "limit_px": 2300.0,
        "order_type": {"trigger": {"triggerPx": 2400.0, "isMarket": True, "tpsl": "sl"}},
        "reduce_only": True}
result = exchange.bulk_orders([entry, stop], grouping="normalTpsl")
print(result)   # Guard's decision comes back in Hyperliquid's format
```

Guard sizes the entry from the stop at 2,400. If it resizes the entry, it resizes the stop with it. Without a stop the entry is refused under the default rules ([why](https://zunderlabs.com/docs/integrations#send-the-stop-with-the-entry)).

## The arguments

| Argument | Value | Note |
|---|---|---|
| `wallet` | an `eth_account` account made from the Guard client key | not your API wallet key |
| `base_url` | Guard's URL | used for `/exchange` and, through `Info`, for `/info` |
| `account_address` | your Hyperliquid account | the account the client key trades for |
| `vault_address` | leave unset | vaults are not supported by Guard yet |

## Things to know

- **Network in the signature.** The SDK signs as mainnet only when `base_url == MAINNET_API_URL` (`"https://api.hyperliquid.xyz"`); for any other URL it signs with the testnet source `"b"` (`construct_phantom_agent` in `signing.py`). Pointed at Guard, it always signs as testnet. Guard is planned to accept that and to sign the real order for the network Guard is set to ([why it is safe](https://zunderlabs.com/docs/integrations#one-thing-every-tool-does-differently-the-network-in-the-signature)).
- **WebSocket.** `Info(GUARD)` without `skip_ws=True` opens a WebSocket at Guard's URL. Guard is planned to pass market-data subscriptions through. Until that is verified, use `skip_ws=True`, or open the WebSocket to Hyperliquid directly: market data needs no key.
- **Builder argument.** `order()` and `bulk_orders()` take an optional `builder`. Leave it unset; Guard adds its own.
