Event schema for the monitor
A WebSocket on Guard’s own address, local only: ws://127.0.0.1:8547/zunder/events. One JSON object per message. Read-only: nothing sent to it changes Guard. Only the kill switch has a write path (planned, separate, and it can only pull, never release).
Envelope
Section titled “Envelope”Every event has:
| Field | Type | Meaning |
|---|---|---|
schema | string | "zunder-guard-event" |
version | integer | 0 while this is a draft |
seq | integer | 1, 2, 3, … per Guard process; a gap means you missed events |
at | integer | UTC epoch milliseconds |
network | string | "paper", "testnet" or "mainnet" |
kind | string | one of the kinds below |
Money is a decimal string, never a JSON number: "0.03144", not 0.03144.
decision
Section titled “decision”One per order that opens, grows or flips a position.
{"schema":"zunder-guard-event","version":0,"seq":42,"at":1791000000000,"network":"paper", "kind":"decision","client":"freqtrade-1","coin":"BTC","side":"buy","effect":"open", "asked_qty":"0.5","stop":"58800", "verdict":{"kind":"resize","qty":"0.03144","notional":"1886.4","rule":"max_loss_per_trade", "source":"engine","reason":"a stop-out may lose at most 2% of equity"}, "rules":[{"rule":"market_allowlist","source":"site","status":"passed"}, {"rule":"protective_stop","source":"site","status":"passed"}, {"rule":"max_leverage","source":"engine","status":"passed"}, {"rule":"min_liquidation_distance","source":"site","status":"passed"}, {"rule":"max_open_risk","source":"engine","status":"passed"}, {"rule":"max_position_size","source":"site","status":"passed"}, {"rule":"max_loss_per_trade","source":"engine","status":"resized"}]}effect:open,increase,flip(as in the website judge).verdict.kind:allow,resize(withqty,notional,rule) orveto(withcode,rule). Codes: Veto and reason codes.rules: every rule checked, in order, up to the first refusal;statusispassed,resized,vetoed,assumedornot_judged.sourcesays where the rule lives:engine,sessionorsite(policy). The namesitecomes from the website judge and may be renamedpolicybefore version 1.
An order that reduces or closes. Never refused, so no verdict: coin, side, qty, client.
What was sent to Hyperliquid and what came back: coin, side, qty, price, role (entry, stop, exit, flatten), status (filled, resting, rejected), venue_message.
placed, moved (tighter), ignored (looser, stays), fired. Fields: coin, from, to.
Whenever the engine’s state changes, and once a minute: state (active, halted_for_day, stopped), equity, peak, day_start, open_risk, open_notional.
halt, resumed, killed
Section titled “halt, resumed, killed”A daily loss stop or drawdown halt fired; a person resumed (note); the kill switch was pulled (reason, by: cli, mcp, telegram, monitor).
discrepancy
Section titled “discrepancy”Guard’s record of positions and the venue’s disagree. discrepancy is one of not_in_book, not_on_venue, side_differs, qty_differs, stop_differs (PositionDiscrepancy in crates/zunder-risk/src/book.rs, exists today).
context, message. Never contains a key; addresses only.
Example: a day in events
Section titled “Example: a day in events”seq 41 risk active, equity 2000, peak 2000seq 42 decision BTC buy 0.5 → resize 0.03144 (max_loss_per_trade)seq 43 order entry filled 0.03144 @ 60000seq 44 stop placed BTC 58800seq 45 decision HYPE buy → veto coin_not_allowedseq 46 stop ignored BTC 58800 → 58000seq 47 stop fired BTC 58800seq 48 risk active, equity 1960