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

# Config keys

Every key of guard.toml with its type, default and bounds, as Guard's code checks them.

{/* Source of truth, checked 6 Oct 2026:
    - top-level keys, defaults and bounds: GuardConfig, its Default and validate() in crates/zunder-guard/src/config.rs
    - [auth]: AuthConfig, DEFAULT_NONCE_MAX_AGE_MS, DEFAULT_NONCE_MAX_AHEAD_MS, MAX_NONCE_*, MAX_CLIENTS in crates/zunder-guard-core/src/auth.rs
    - [policy]: Policy, its Default, the *_BOUNDS constants, validate() and within_mainnet_ceiling() in crates/zunder-guard-core/src/policy.rs
    - the engine's defaults behind the policy's: RiskLimits::default() and MAX_TRADING_EQUITY_USD in crates/zunder-risk/src/limits.rs
    - what init writes: crates/zunder-guard/src/init.rs
    Not the website's judge (crates/zunder-risk-wasm/src/judge.rs): its keys and bounds are different.
    Change this page in the same commit as any of these. */}

:::note[Planned]
Guard has no release yet. These are the keys of `guard.toml` in Guard's source today; they may still change before the first release.
:::

`guard.toml` lives in Guard's home (`~/.zunder-guard` unless `--home` or `ZUNDER_GUARD_HOME` says otherwise; `--config` names another file). `zunder-guard init` writes it; `zunder-guard check-config` checks it and prints its rules.

## Conventions

- **Fractions are fractions.** `0.02` means 2%. Basis points end in `_bps`.
- **Decimals are strings.** `max_loss_at_stop = "0.02"`, not `0.02`. A bare float is refused, and so is a TOML date, so no value is rounded on the way in.
- **Unknown keys are refused**, so a typo is an error, not a silently ignored setting.
- **Every value is checked** at start, against an upper bound as well as a lower one. A value out of bounds is refused, never clamped.
- **A missing key takes its default**, except `network`, `account` and `[auth] clients`, which have none.

## Example

What `init` writes for a paper-mode Guard that reads a testnet account, with the default rules. Your file has your own addresses.

```toml
network = "testnet"
mode = "paper"
account = "0xYourAccountAddress"
allow_mainnet = false
listen = "127.0.0.1:8547"
state_dir = "."
sync_seconds = 5
ip_share = "1"
pairing_sha3 = "…64 hex digits…"

[auth]
clients = ["0xTheClientKeysAddress"]
nonce_max_age_ms = 30000
nonce_max_ahead_ms = 5000

[policy]
max_leverage = "5"
max_loss_at_stop = "0.02"
stop = "attach"
default_stop_distance = "0.02"
min_liquidation_distance = "0.10"
max_position_of_account = "2"
max_open_risk = "0.06"
daily_loss_stop = "0.06"
drawdown_halt = "0.25"
markets = "all"
entry_price_bound = "0.005"
fee_bps = "4.5"
slippage_bps = "1"
stop_slippage = "0.10"
exit_slippage = "0.05"
```

## Top-level keys

`GuardConfig` in `crates/zunder-guard/src/config.rs`.

| Key | Type | Default | Bounds and checks |
|---|---|---|---|
| `network` | string | none: required | `"testnet"` or `"mainnet"`: the network of the account |
| `mode` | string | `"paper"` | `"paper"`, `"testnet"` or `"mainnet"`. A sending mode must be the account's `network`. `run` sends only with the same `--network` on its command line |
| `account` | string | none: required | `0x` and 40 hex digits: the main wallet's address, not the API wallet's |
| `api_wallet` | string | unset | `0x` and 40 hex digits. Needed to send: the key Guard is given must be this API wallet's. `key check` records it |
| `allow_mainnet` | boolean | `false` | `true` only in a mainnet config; refused in a testnet config |
| `listen` | string | `"127.0.0.1:8547"` | an IP address with a port, loopback only. `run --listen` overrides it at start |
| `state_dir` | path | `"guard-state"` (`init` writes `"."`) | not empty. A relative path is taken from the config file's directory. Holds the journals and the kill file `kill` |
| `sync_seconds` | integer | `5` | 5 to 300: seconds between two reads of the account (Guard reads less often when its `ip_share` or its HIP-3 dexes need it) |
| `ip_share` | decimal | `"1"` | above 0, at most 1: the part of the IP address's request weight (Hyperliquid's 1,200 a minute) this Guard may spend. **Several Guards on one machine: `1/N` each, as a decimal (`"0.5"`, `"0.3333"`).** Every budget is fitted to it; a share too small to keep Guard safe is refused: below 0.291 with the main dex alone (three Guards per address at most), 0.466 with one HIP-3 dex (two), 0.749 with two (one). `init --ip-share` writes it, `run --ip-share` overrides it |
| `emergency_dir` | path | `<state_dir>/emergency` | not `state_dir` itself. Where Guard records the protective actions it sends (flattening, stops, closes) while the decision journal cannot be written, in `emergency-` and the journal's file name; best a directory on another disk that Guard may write. A relative path is taken from the config file's directory |
| `licence` | string | unset | a licence key (`zgl1_…`); not a secret |
| `pairing_sha3` | string | unset | 64 hex digits: the hash of the pairing code `init` or `pair` showed once |

## `[auth]`

`AuthConfig` in `crates/zunder-guard-core/src/auth.rs`.

| Key | Type | Default | Bounds |
|---|---|---|---|
| `clients` | array of strings | none: at least one | 1 to 64 addresses, `0x` and 40 hex digits each: the client keys' addresses (`init`, `pair` and `client add` add them) |
| `nonce_max_age_ms` | integer | `30000` | 1,000 to 300,000: how old a request's nonce may be |
| `nonce_max_ahead_ms` | integer | `5000` | 100 to 60,000: how far ahead of Guard's clock a nonce may be |

## `[policy]`

`Policy` in `crates/zunder-guard-core/src/policy.rs`. The first ten keys are the [nine rules](https://zunder-design-preview.pages.dev/docs/concepts/rules) (the stop rule has two); they travel in a [rules code](https://zunder-design-preview.pages.dev/docs/reference/rules-schema). The rest are Guard's cost and execution assumptions, which no rules code carries.

| Key | Type | Default | Bounds | Rule |
|---|---|---|---|---|
| `max_leverage` | decimal | `"5"` | above 0, at most 10, at most four decimal places | (c) max leverage |
| `max_loss_at_stop` | decimal | `"0.02"` | above 0, at most 0.05; at most `max_open_risk` | (g) max loss at the stop |
| `stop` | string | `"attach"` | `"attach"` or `"refuse"` | (b) an entry without a stop |
| `default_stop_distance` | decimal | `"0.02"` | above 0, at most 0.5; below `min_liquidation_distance` | (b) where an attached stop goes |
| `min_liquidation_distance` | decimal | `"0.10"` | 0.01 to 0.5; above `default_stop_distance` | (d) |
| `max_position_of_account` | decimal | `"2"` | above 0, at most 10; at most `max_leverage` | (f) max position, 2 is 200% of equity |
| `max_open_risk` | decimal | `"0.06"` | above 0, at most 0.2; at least `max_loss_at_stop` | (e) |
| `daily_loss_stop` | decimal | `"0.06"` | above 0, at most 0.15 | (h) |
| `drawdown_halt` | decimal | `"0.25"` | above 0, at most 0.5 | (i) |
| `markets` | `"all"` or array of strings | `"all"` | `"all"`, or 1 to 32 market names (1 to 32 characters each) | (a) |
| `entry_price_bound` | decimal | `"0.005"` | 0.0005 to 0.05 | how far beyond the mid an entry's limit may lie; a limit further out is pulled in |
| `fee_bps` | decimal | `"4.5"` | 0 to 50 | taker fee per side, counted into each entry's risk |
| `slippage_bps` | decimal | `"1"` | 0 to 100 | expected slippage per side, counted likewise |
| `stop_slippage` | decimal | `"0.10"` | 0.05 to 0.2 | how far beyond its trigger Guard's market stop may fill; the liquidation must lie beyond that |
| `exit_slippage` | decimal | `"0.05"` | 0.01 to 0.1 | how far from the mid Guard's own closing orders may fill when it flattens |
| `max_trading_equity_usd` | decimal | unset (no cap) | above 0, at most 2500 | the [equity cap](https://zunder-design-preview.pages.dev/docs/concepts/equity-cap), in USDC |

`max_loss_at_stop`, `default_stop_distance`, `min_liquidation_distance`, `max_position_of_account`, `max_open_risk`, `daily_loss_stop` and `drawdown_halt` take at most six decimal places (four in percent, as a rules code writes them).

## Mainnet

A config with `mode = "mainnet"` is checked further (`check_mainnet` in `config.rs`):

- `allow_mainnet = true`, `account`, `api_wallet` and `max_trading_equity_usd` are all required;
- no rule may be looser than the mainnet ceiling (`Policy::within_mainnet_ceiling`; today equal to the defaults, but pinned on its own, so changing a default never loosens mainnet): `max_leverage`, `max_loss_at_stop`, `max_open_risk`, `daily_loss_stop`, `drawdown_halt`, `max_position_of_account`, `entry_price_bound` and `default_stop_distance` at most the ceiling; `min_liquidation_distance`, `fee_bps`, `slippage_bps`, `stop_slippage` and `exit_slippage` at least it; no HIP-3 market.

A config alone never sends to mainnet: `run` also needs `--network mainnet`, the key on standard input and `ZUNDER_MAINNET_CONFIRM` naming the account ([Paper, testnet and mainnet](https://zunder-design-preview.pages.dev/docs/concepts/networks#mainnet)).

## Not in `guard.toml`

There is no `[risk]` section, no relay and no telemetry setting. The keys `risk_per_trade`, `drawdown_stop`, `stop_policy`, `max_position_fraction`, `round_trip_cost_bps` and `allowed_coins` belong to the risk engine's own config and to the website's judge, not to Guard; in `guard.toml` they are refused as unknown keys.
