ccxt
The change
Section titled “The change”ccxt builds every Hyperliquid URL from urls['api']['public'] and urls['api']['private'] (hyperliquid.sign()). Anything you pass to the constructor is deep-merged over ccxt’s defaults (Exchange.__init__: settings = self.deep_extend(self.describe(), config)). So the URL override goes into the constructor.
exchange = ccxt.hyperliquid({ "walletAddress": "0xYourAccountAddress", "privateKey": "0x…API wallet key…", "privateKey": "0x…client key from Guard…", "urls": {"api": {"public": "http://127.0.0.1:8547", "private": "http://127.0.0.1:8547"}, "test": {"public": "http://127.0.0.1:8547", "private": "http://127.0.0.1:8547"}}, "options": {"builderFee": False, "refSet": True},})Full example (Python)
Section titled “Full example (Python)”import ccxt
GUARD = "http://127.0.0.1:8547"
exchange = ccxt.hyperliquid({ "walletAddress": "0xYourAccountAddress", # your main account, not the API wallet "privateKey": "0x...", # the client key Guard printed "urls": { "api": {"public": GUARD, "private": GUARD}, "test": {"public": GUARD, "private": GUARD}, }, "options": {"builderFee": False, "refSet": True},})
# The entry and its stop in one request (Hyperliquid grouping "normalTpsl").order = exchange.create_order( "BTC/USDC:USDC", "limit", "buy", 0.5, 60000, params={"stopLoss": {"triggerPrice": 58800, "type": "market"}},)filled in from your settings ·
The stopLoss parameter makes ccxt send the entry and a reduce-only stop together, with grouping normalTpsl (create_orders_request()). Guard sizes the entry from that stop. Without a stop the entry is refused under the default rules (why).
JavaScript takes the same keys: new ccxt.hyperliquid({ walletAddress, privateKey, urls, options }).
The config keys
Section titled “The config keys”| Key | Value | Why |
|---|---|---|
walletAddress | your Hyperliquid account address | ccxt requires it (requiredCredentials); Guard checks it is the account it guards |
privateKey | the Guard client key | Hyperliquid does not accept it; only your Guard does |
urls.api.public, urls.api.private | Guard’s URL | every request goes to Guard |
urls.test.public, urls.test.private | Guard’s URL | ccxt’s set_sandbox_mode(True) replaces urls['api'] with urls['test']. Without this override, turning on sandbox mode would send orders straight to Hyperliquid testnet, around Guard |
options.builderFee | False | see below |
options.refSet | True | see below |
Two ccxt defaults behind Guard
Section titled “Two ccxt defaults behind Guard”On its first private call ccxt runs initialize_client(), which does two things you do not want behind Guard:
handle_builder_fee_approval()approves ccxt’s own builder address (0x6530…27a6) atfeeRate(default0.01%). WithbuilderFee: Falseit still approves, at 0%. The approval is a user-signed action, which Guard refuses (only your main wallet can approve a builder fee). ccxt catches the error and setsbuilderFeetoFalse;approvedBuilderFeestaysFalse, so ccxt attaches no builder to orders.set_ref()sendssetReferrerwith codeCCXT1. Guard refuses it as a non-trading action. ccxt ignores the error.refSet: Trueskips the request.
Both refusals are harmless; the options only save two refused requests at start-up.
What to expect
Section titled “What to expect”- An oversized order comes back filled at a smaller size. Read the filled amount; do not assume the size you sent.
- A refused order raises a ccxt
ExchangeErrorwhose message carries Guard’s reason and veto code. fetch_balance,fetch_positionsand market data pass through to Hyperliquid unchanged.
Sources
Section titled “Sources”- ccxt,
python/ccxt/hyperliquid.py:describe()(urls,requiredCredentials,options),sign(),initialize_client(),handle_builder_fee_approval(),set_ref(),create_orders_request(). - ccxt,
python/ccxt/base/exchange.py:Exchange.__init__,set_sandbox_mode(). - ccxt manual, Hyperliquid.