ccxt
Put Guard in front of a ccxt Hyperliquid client by overriding its API URLs. The exact config keys, and two ccxt defaults to switch off.
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.
This page as plain Markdown, for people and LLMs: /docs/integrations/ccxt.md