← back to Ken
Ken: add /trading real-money on-off switch (UI toggle = source of truth)
2191a43d39ed793a68455feb90b1dd67d7f6b2aa · 2026-08-03 12:05:18 -0700 · Steve Abrams
New auth-gated /trading page + POST /api/trading/mode action=set_trading that atomically sets risk_state.config.trading_on + safe_mode (+auto-trader). Rewired ken-safemode-guard.sh from 'always hold safe_mode=true' to SOURCE-OF-TRUTH: it holds safe_mode == !trading_on, auto-heals + alerts on unauthorized drift in EITHER direction, and never-silent alerts when armed / when a real trade lands. Bootstrapped the guard via launchd (it had died). Default OFF/safe; arming stays a human toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M kalshi-dash/ken-safemode-guard.shM kalshi-dash/server.jsA kalshi-dash/trading-switch.html
Diff
commit 2191a43d39ed793a68455feb90b1dd67d7f6b2aa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 12:05:18 2026 -0700
Ken: add /trading real-money on-off switch (UI toggle = source of truth)
New auth-gated /trading page + POST /api/trading/mode action=set_trading that atomically sets risk_state.config.trading_on + safe_mode (+auto-trader). Rewired ken-safemode-guard.sh from 'always hold safe_mode=true' to SOURCE-OF-TRUTH: it holds safe_mode == !trading_on, auto-heals + alerts on unauthorized drift in EITHER direction, and never-silent alerts when armed / when a real trade lands. Bootstrapped the guard via launchd (it had died). Default OFF/safe; arming stays a human toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
kalshi-dash/ken-safemode-guard.sh | 73 +++++++++++++++++--------
kalshi-dash/server.js | 30 +++++++++++
kalshi-dash/trading-switch.html | 111 ++++++++++++++++++++++++++++++++++++++
3 files changed, 193 insertions(+), 21 deletions(-)
diff --git a/kalshi-dash/ken-safemode-guard.sh b/kalshi-dash/ken-safemode-guard.sh
index 312630a..6800dbf 100755
--- a/kalshi-dash/ken-safemode-guard.sh
+++ b/kalshi-dash/ken-safemode-guard.sh
@@ -1,40 +1,71 @@
#!/bin/bash
-# ken-safemode-guard.sh — TIGHT AUTO-HEAL WATCHDOG (Steve-authorized 2026-08-03).
-# Holds Ken's safe_mode=true (his "go live SAFE MODE ON" state). If an external actor
-# flips safe_mode OFF (real-money armed), instantly restores it to TRUE, logs the heal,
-# and alerts (CNCP parking-lot card + George email). ONLY ever writes safe_mode=TRUE —
-# never off. Also loudly flags if a real trade (ken_trades>0) ever lands.
-# Does NOT touch live_run.active / trade_config.enabled — the live loop stays as Steve set it.
+# ken-safemode-guard.sh — SOURCE-OF-TRUTH AUTO-HEAL WATCHDOG (Steve-authorized 2026-08-03,
+# rewired to respect the UI Trading Switch, same date).
+#
+# The authoritative intent is risk_state.config.trading_on (set ONLY by the auth-gated
+# /trading UI switch, action=set_trading). The guard holds safe_mode CONSISTENT with it:
+# trading_on=true -> desired safe_mode=false (real money armed, HELD armed)
+# trading_on=false -> desired safe_mode=true (safe, HELD safe) [default when unset]
+# If actual safe_mode drifts from desired (an UNAUTHORIZED flip in EITHER direction),
+# the guard instantly heals it back to the intent, logs, and alerts (CNCP + George).
+# This preserves rogue-flip protection while letting Steve's toggle be the real on/off.
+# Also: first time it observes an ARMED state, and the first time a real trade lands,
+# it alerts so real-money-live is NEVER silent. Does NOT touch live_run/trade_config.
set -uo pipefail
DIR="$HOME/.claude/skills/ken-gambling-canary/data"; mkdir -p "$DIR"
LOG="$DIR/safemode-guard.log"; STATE="$DIR/safemode-guard-state.json"
TO="${GUARD_TO:-steve@designerwallcoverings.com}"; CNCP="${CNCP_URL:-http://localhost:3333}"
INTERVAL="${GUARD_INTERVAL:-30}"
log(){ echo "[$(date -Iseconds)] $1" | tee -a "$LOG" >&2; }
-log "GUARD START (interval ${INTERVAL}s) — holding safe_mode=true; auto-heal + alert on flip/trade"
-HEALS=0; TRADE_ALERTED=0
+alert(){ # $1=url-key $2=subject $3=body(html)
+ curl -sS --max-time 10 "$CNCP/api/parking-lot" -H 'Content-Type: application/json' \
+ -d "$(jq -n --arg u "$1" --arg note "$3" '{url:$u,note:$note}')" >/dev/null 2>&1 || true
+ if [ -f "$HOME/.claude/skills/_shared/george-send.sh" ]; then
+ . "$HOME/.claude/skills/_shared/george-send.sh"
+ george_send steve-office "$TO" "$2" "<div style=\"font-family:-apple-system,sans-serif\">$3</div>" >/dev/null 2>&1 || true
+ fi
+}
+log "GUARD START (interval ${INTERVAL}s) — SOURCE-OF-TRUTH mode: hold safe_mode == !trading_on; heal+alert on drift"
+HEALS=0; TRADE_ALERTED=0; ARMED_ALERTED=0
while true; do
+ INTENT=$(psql -d bertha_betting -tAc "SELECT COALESCE(config->>'trading_on','false') FROM risk_state ORDER BY updated_at DESC LIMIT 1;" 2>/dev/null | tr -d '[:space:]')
SM=$(psql -d bertha_betting -tAc "SELECT config->>'safe_mode' FROM risk_state ORDER BY updated_at DESC LIMIT 1;" 2>/dev/null | tr -d '[:space:]')
TR=$(psql -d ken -tAc "SELECT count(*) FROM ken_trades;" 2>/dev/null | tr -d '[:space:]')
- if [ "$SM" = "false" ]; then
- psql -d bertha_betting -tAc "UPDATE risk_state SET config=jsonb_set(config,'{safe_mode}','true'),updated_at=NOW() WHERE id=(SELECT id FROM risk_state ORDER BY updated_at DESC LIMIT 1);" >/dev/null 2>&1
+ # Skip a cycle on DB read failure — never heal on unknown state.
+ if [ -z "$SM" ] || { [ "$INTENT" != "true" ] && [ "$INTENT" != "false" ]; }; then sleep "$INTERVAL"; continue; fi
+
+ DESIRED="true"; [ "$INTENT" = "true" ] && DESIRED="false" # desired safe_mode = !trading_on
+
+ if [ "$SM" != "$DESIRED" ]; then
+ psql -d bertha_betting -tAc "UPDATE risk_state SET config=jsonb_set(config,'{safe_mode}','${DESIRED}'),updated_at=NOW() WHERE id=(SELECT id FROM risk_state ORDER BY updated_at DESC LIMIT 1);" >/dev/null 2>&1
HEALS=$((HEALS+1))
- log "HEAL #$HEALS — safe_mode was FALSE, restored to true (trades=$TR)"
- NOTE="[KEN SAFE-MODE HEAL $(date +%H:%M)] safe_mode was flipped OFF (real-money armed) and auto-restored to true by ken-safemode-guard. trades=$TR. Total heals=$HEALS. Something is re-arming Ken — investigate the actor."
- curl -sS --max-time 10 "$CNCP/api/parking-lot" -H 'Content-Type: application/json' \
- -d "$(jq -n --arg u "ken://safemode-heal" --arg note "$NOTE" '{url:$u,note:$note}')" >/dev/null 2>&1 || true
- if [ -f "$HOME/.claude/skills/_shared/george-send.sh" ]; then
- . "$HOME/.claude/skills/_shared/george-send.sh"
- BODY="<div style=\"font-family:-apple-system,sans-serif\"><h3 style=\"color:#b23b3b\">Ken safe_mode was flipped OFF - auto-restored</h3><div>An external actor set <b>safe_mode=false</b> (real-money armed). The guard restored it to <b>true</b> within ${INTERVAL}s. Real trades so far: <b>${TR}</b>. Total heals this run: <b>${HEALS}</b>.</div><p style=\"color:#666;font-size:13px\">The live loop (live_run.active/trade_config.enabled) is untouched; only safe_mode is being held true. Investigate what keeps re-arming Ken.</p></div>"
- george_send steve-office "$TO" "Ken safe_mode flipped OFF - auto-restored (heal #$HEALS)" "$BODY" >/dev/null 2>&1 || true
+ if [ "$DESIRED" = "true" ]; then
+ log "HEAL #$HEALS — UNAUTHORIZED ARM: safe_mode was OFF but trading_on=false; re-braked (trades=$TR)"
+ alert "ken://safemode-heal" "Ken safe_mode flipped OFF without the switch - re-braked (heal #$HEALS)" \
+ "<h3 style=\"color:#b23b3b\">Unauthorized arm auto-reverted</h3><div>safe_mode was set <b>false</b> while the Trading Switch is <b>OFF</b> (trading_on=false). The guard restored <b>safe_mode=true</b> within ${INTERVAL}s. Real trades: <b>${TR}</b>. Heals: <b>${HEALS}</b>. Investigate who armed Ken outside the /trading switch.</div>"
+ else
+ log "HEAL #$HEALS — safe_mode drifted ON while switch is ON (trading_on=true); re-armed to match intent (trades=$TR)"
+ alert "ken://safemode-heal" "Ken safe_mode drifted OFF-intent - restored to ARMED (heal #$HEALS)" \
+ "<h3 style=\"color:#b26b3b\">Re-armed to match the Trading Switch</h3><div>safe_mode was <b>true</b> but the Trading Switch is <b>ON</b> (trading_on=true). The guard restored <b>safe_mode=false</b>. Real trades: <b>${TR}</b>.</div>"
fi
fi
+
+ # Never-silent: first observation of an armed state.
+ if [ "$INTENT" = "true" ] && [ "$ARMED_ALERTED" = "0" ]; then
+ ARMED_ALERTED=1
+ log "REAL MONEY LIVE — Trading Switch is ON (safe_mode held OFF). trades=$TR"
+ alert "ken://armed" "Ken is LIVE - real-money trading switched ON" \
+ "<h3 style=\"color:#1f9d55\">Trading Switch ON</h3><div>Real-money trading is <b>armed and held ON</b> by your /trading switch (safe_mode=false). The engine places real Kalshi orders on winner-consensus, capped \$2/pos · \$10/day. Toggle OFF at /trading to brake.</div>"
+ fi
+ [ "$INTENT" != "true" ] && ARMED_ALERTED=0 # reset so a future ON re-alerts
+
if [ -n "$TR" ] && [ "$TR" -gt 0 ] 2>/dev/null && [ "$TRADE_ALERTED" = "0" ]; then
TRADE_ALERTED=1
log "REAL TRADES DETECTED: ken_trades=$TR"
- curl -sS --max-time 10 "$CNCP/api/parking-lot" -H 'Content-Type: application/json' \
- -d "$(jq -n --arg u "ken://real-trades" --arg note "[KEN REAL TRADES $(date +%H:%M)] ken_trades=$TR — real Kalshi orders are on the books. safe_mode=$SM." '{url:$u,note:$note}')" >/dev/null 2>&1 || true
+ alert "ken://real-trades" "Ken real trades on the books ($TR)" \
+ "<h3>Real Kalshi orders placed</h3><div>ken_trades=<b>$TR</b>. safe_mode=$SM, trading_on=$INTENT.</div>"
fi
- echo "{\"ts\":\"$(date -Iseconds)\",\"safe_mode\":\"$SM\",\"trades\":\"$TR\",\"heals\":$HEALS}" > "$STATE"
+
+ echo "{\"ts\":\"$(date -Iseconds)\",\"trading_on\":\"$INTENT\",\"safe_mode\":\"$SM\",\"desired\":\"$DESIRED\",\"trades\":\"$TR\",\"heals\":$HEALS}" > "$STATE"
sleep "$INTERVAL"
done
diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index 71dd5dc..bec0c64 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -1442,6 +1442,20 @@ const routes = {
return json(res, { success: true, trade_config: TRADE_CONFIG });
}
+ if (body.action === 'set_trading') {
+ // Unified real-money on/off — the SINGLE SOURCE OF TRUTH the safe_mode guard now
+ // respects (ken-safemode-guard.sh reads config.trading_on and holds safe_mode to match).
+ // ON => trading_on:true, safe_mode:false, auto-trader enabled.
+ // OFF => trading_on:false, safe_mode:true, auto-trader disabled.
+ const on = !!body.on;
+ const newConfig = { ...cfg, safe_mode: !on, trading_on: on };
+ await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+ TRADE_CONFIG.enabled = on;
+ await saveTradeConfig();
+ console.log(`[Ken] TRADING ${on ? 'ON — real money armed (safe_mode OFF)' : 'OFF — safe (safe_mode ON)'} via UI switch`);
+ return json(res, { success: true, trading_on: on, safe_mode: !on, auto_trader_enabled: on });
+ }
+
json(res, { error: `Unknown trading mode action: ${body.action}` }, 400);
} catch (err) {
json(res, { error: err.message }, 500);
@@ -5005,6 +5019,22 @@ const server = http.createServer(async (req, res) => {
return;
}
+ // ── Trading Switch — real-money on/off toggle (auth-gated) ──
+ if (urlPath === '/trading' || urlPath === '/trading/') {
+ if (!isAuthenticated(req)) {
+ return json(res, { error: 'Authentication required' }, 401);
+ }
+ try {
+ const html = fs.readFileSync(path.join(__dirname, 'trading-switch.html'), 'utf8');
+ res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
+ res.end(html);
+ } catch (e) {
+ res.writeHead(500);
+ res.end('Trading switch page not found: ' + e.message);
+ }
+ return;
+ }
+
// ── Serve Static Files / SPA Routing ──
let filePath = path.join(DIST, urlPath === '/' ? 'index.html' : urlPath);
diff --git a/kalshi-dash/trading-switch.html b/kalshi-dash/trading-switch.html
new file mode 100644
index 0000000..a3ddcc4
--- /dev/null
+++ b/kalshi-dash/trading-switch.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>Ken — Trading Switch</title>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
+<style>
+ :root{ --bg:#0b0e14; --card:#141a24; --line:#232c3a; --ink:#e6ebf2; --dim:#8a97a8;
+ --green:#22c55e; --red:#ef4444; --amber:#f59e0b; }
+ *{box-sizing:border-box} html,body{margin:0;background:var(--bg);color:var(--ink);
+ font-family:Inter,system-ui,sans-serif}
+ .wrap{max-width:560px;margin:0 auto;padding:28px 18px}
+ h1{font-family:'Space Grotesk',sans-serif;font-size:20px;letter-spacing:.02em;margin:0 0 2px}
+ .sub{color:var(--dim);font-size:13px;margin-bottom:22px}
+ .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:22px;margin-bottom:16px}
+ .switchrow{display:flex;align-items:center;justify-content:space-between;gap:16px}
+ .state{font-family:'Space Grotesk',sans-serif;font-size:26px;font-weight:700}
+ .state.on{color:var(--green)} .state.off{color:var(--dim)}
+ .toggle{position:relative;width:96px;height:52px;border-radius:52px;border:1px solid var(--line);
+ background:#1c2430;cursor:pointer;transition:background .18s;flex:none}
+ .toggle[data-on="true"]{background:rgba(34,197,94,.22);border-color:var(--green)}
+ .knob{position:absolute;top:4px;left:4px;width:42px;height:42px;border-radius:50%;
+ background:var(--dim);transition:left .18s,background .18s}
+ .toggle[data-on="true"] .knob{left:48px;background:var(--green)}
+ .toggle[data-busy="true"]{opacity:.5;pointer-events:none}
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px}
+ .kv{background:#0f141c;border:1px solid var(--line);border-radius:9px;padding:10px 12px}
+ .kv .k{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.04em}
+ .kv .v{font-family:'JetBrains Mono',monospace;font-size:15px;margin-top:3px}
+ .warn{background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.35);color:#fca5a5;
+ border-radius:10px;padding:11px 13px;font-size:13px;margin-top:14px;display:none}
+ .warn.show{display:block}
+ .note{background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.3);color:#fcd34d;
+ border-radius:10px;padding:11px 13px;font-size:12.5px;margin-top:12px;display:none}
+ .note.show{display:block}
+ .foot{color:var(--dim);font-size:11.5px;margin-top:18px;line-height:1.6}
+ a{color:#7dd3fc}
+ .pulse{width:9px;height:9px;border-radius:50%;display:inline-block;margin-right:7px;vertical-align:middle}
+ .pulse.on{background:var(--green);box-shadow:0 0 0 0 rgba(34,197,94,.6);animation:p 1.6s infinite}
+ .pulse.off{background:var(--dim)}
+ @keyframes p{0%{box-shadow:0 0 0 0 rgba(34,197,94,.5)}70%{box-shadow:0 0 0 9px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
+</style>
+</head>
+<body>
+<div class="wrap">
+ <h1>Ken — Trading Switch</h1>
+ <div class="sub">The single source of truth for real-money trading. The <code>safe_mode</code> guard now holds whatever this toggle sets.</div>
+
+ <div class="card">
+ <div class="switchrow">
+ <div><span class="pulse off" id="pulse"></span><span class="state off" id="stateLabel">…</span></div>
+ <div class="toggle" id="toggle" data-on="false" role="switch" aria-checked="false" tabindex="0"><div class="knob"></div></div>
+ </div>
+ <div class="warn" id="warn">⚠ <b>Real money armed.</b> The engine will place real Kalshi orders when ≥2 proven winner-bots agree, capped at $2/position · $10/day · $4 daily-loss.</div>
+ <div class="note" id="note">Armed, but <b>no winner-consensus yet</b> — the fresh sim needs ~50 resolved trades/bot before real fills can fire. Armed ≠ trading until then.</div>
+ <div class="grid" id="grid"></div>
+ </div>
+
+ <div class="foot" id="foot"></div>
+</div>
+<script>
+const api = (body)=>fetch('/api/trading/mode',{method:'POST',credentials:'same-origin',
+ headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json());
+const liveRun = ()=>fetch('/api/live-run',{credentials:'same-origin'}).then(r=>r.json()).catch(()=>({}));
+const $ = id=>document.getElementById(id);
+let state = {};
+
+function render(s, lr){
+ const on = s.safe_mode === false; // trading ON == safe_mode OFF
+ const tog = $('toggle'); tog.dataset.on = on; tog.setAttribute('aria-checked', on);
+ $('stateLabel').textContent = on ? 'TRADING ON' : 'TRADING OFF';
+ $('stateLabel').className = 'state ' + (on?'on':'off');
+ $('pulse').className = 'pulse ' + (on?'on':'off');
+ $('warn').className = 'warn' + (on?' show':'');
+ const tc = s.trade_config || {}, ts = s.trader_state || {};
+ const kv = [
+ ['Master brake (safe_mode)', on ? 'OFF (armed)' : 'ON (safe)'],
+ ['Auto-trader', s.auto_trader_enabled ? 'enabled' : 'disabled'],
+ ['Kalshi', (s.env||'?').toUpperCase() + (s.kalshi_connected?' · connected':' · no keys')],
+ ['Live run', lr && lr.active ? ('day '+lr.day+'/'+lr.days) : 'inactive'],
+ ['Spent today', lr ? ('$'+((lr.spent_today_cents||0)/100).toFixed(2)+' / $'+((lr.daily_budget_cents||0)/100).toFixed(2)) : '—'],
+ ['Open positions', (ts.openCount!=null?ts.openCount:'—') + ' / ' + (tc.max_open_positions||'—')],
+ ['Per-position cap', '$'+(((tc.max_position_cents)||0)/100).toFixed(2)],
+ ['Daily-loss limit', '$'+(((tc.daily_loss_limit_cents)||0)/100).toFixed(2)],
+ ];
+ $('grid').innerHTML = kv.map(([k,v])=>`<div class="kv"><div class="k">${k}</div><div class="v">${v}</div></div>`).join('');
+ // consensus caveat only matters when armed
+ $('note').className = 'note' + (on ? ' show' : '');
+}
+
+async function refresh(){
+ try{ const [s,lr] = await Promise.all([api({action:'get'}), liveRun()]); state=s; render(s,lr);
+ $('foot').innerHTML = 'Guard: source-of-truth mode — flips inconsistent with this switch are auto-healed + alerted. '+
+ 'Real trades so far: <b>'+((state.trader_state&&state.trader_state.realTrades)||0)+'</b>. Auto-refresh 5s.'; }
+ catch(e){ $('foot').textContent = 'load error: '+e.message; }
+}
+
+async function setTrading(on){
+ if(on && !confirm('Turn REAL-MONEY trading ON?\n\nThe $50 live run will place real Kalshi orders once ≥2 winner-bots agree (capped $2/pos · $10/day). The safe_mode guard will HOLD it on until you toggle off.')) return;
+ const tog=$('toggle'); tog.dataset.busy='true';
+ try{ await api({action:'set_trading', on}); await refresh(); }
+ catch(e){ alert('failed: '+e.message); }
+ tog.dataset.busy='false';
+}
+$('toggle').addEventListener('click', ()=> setTrading(state.safe_mode !== false ? true : false));
+$('toggle').addEventListener('keydown', e=>{ if(e.key===' '||e.key==='Enter'){e.preventDefault(); $('toggle').click();} });
+refresh(); setInterval(refresh, 5000);
+</script>
+</body>
+</html>
← 416f69e auto-save: 2026-08-03T11:53:46 (1 files) — kalshi-dash/packa
·
back to Ken
·
Ken: style the /trading toggle as a large brass AB bat-switc ea9bacb →