<!-- https://zunder-design-preview.pages.dev/docs/integrations/ccxt · Markdown version of the page -->

# 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.

:::note[Planned]
Guard 1.0 integration, not verified against a running Guard yet. Written from ccxt's source, `python/ccxt/hyperliquid.py` and `python/ccxt/base/exchange.py` on the `master` branch, read 6 Oct 2026 (`handle_builder_fee_approval()`, `set_ref()` and the builder field in `create_orders_request()` checked again that day).
:::

## Before you change anything

- Read [the setup journey](https://zunder-design-preview.pages.dev/docs/deploy) and start Guard in paper mode.
- Keep your real account address. Give the bot a **Guard client key**, while the API wallet key stays inside Guard.
- Check the network and isolated-margin requirements in this guide. These integration instructions are planned and not verified against a released Guard yet.

## The smallest 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.

```diff
 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)

```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"}},
)
```

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, Guard attaches its own under the default rules, or refuses the entry if your rules say `refuse` ([why the stop belongs with the entry](https://zunder-design-preview.pages.dev/docs/integrations#send-the-stop-with-the-entry)).

JavaScript takes the same keys: `new ccxt.hyperliquid({ walletAddress, privateKey, urls, options })`.

## The config keys

| Key | Value | Why |
|---|---|---|
| `walletAddress` | your Hyperliquid account address | ccxt requires it (`requiredCredentials`) and uses it as the `user` of its account queries; it must be the account Guard guards, or ccxt reads another account's balance and positions |
| `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 |
| `options.approvedBuilderFee` | leave it unset | **never `True`**: see below |

## 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()`** sends `approveBuilderFee` for ccxt's own builder address (`0x6530…27a6`) at `feeRate` (default `0.01%`; `0%` with `builderFee: False`), unless `options.approvedBuilderFee` is already `True`. Guard refuses it (`funds_or_permissions`: only your main wallet can approve a builder fee). ccxt catches the error and sets `builderFee` to `False`; `approvedBuilderFee` stays `False`.
- **`set_ref()`** sends `setReferrer` with code `CCXT1`. Guard refuses it (`funds_or_permissions`). ccxt ignores the error. `refSet: True` skips the request.

**Do not set `approvedBuilderFee: True`.** ccxt attaches its builder field to every order whenever `approvedBuilderFee` is `True` (at `f = 0` with `builderFee: False`), and Guard vetoes any order that carries a builder field of its own (`client_builder`). Left unset, the one refused approval at start-up keeps it `False`, and ccxt attaches nothing.

## What to expect

- Guard puts every entry on **isolated margin** itself, at a leverage that keeps the liquidation beyond the stop. Do not switch the market to cross margin (`set_margin_mode("cross")` or a cross `set_leverage`): Guard refuses it.
- An entry without a stop is not refused under the default rules: Guard attaches its own stop 2% away (`stop = "attach"`) and sizes from it. Sending your own stop with the entry, as above, lets Guard size from yours.
- 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 `ExchangeError` whose message carries Guard's reason and [veto code](https://zunder-design-preview.pages.dev/docs/reference/veto-codes).
- `fetch_balance`, `fetch_positions` and market data pass through to Hyperliquid unchanged.

## 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](https://docs.ccxt.com/#/exchanges/hyperliquid).

## Read the response

The reply uses Hyperliquid’s response format. Check the returned status and order size rather than assuming the requested size was accepted. A refusal includes a readable reason and a [veto code](https://zunder-design-preview.pages.dev/docs/reference/veto-codes); a smaller accepted order reflects the rule that bound its size. Paper mode sends no venue order. Test this path before using testnet.
