> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alterscope.org/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket Playground

> Connect to Alterscope risk-event and factor streams from the browser.

export function WebSocketPlayground({path, filterFields = [], description}) {
  const [host, setHost] = useState(SERVERS[0].host);
  const [apiKey, setApiKey] = useState("");
  const [filters, setFilters] = useState({});
  const [messages, setMessages] = useState([]);
  const [status, setStatus] = useState("idle");
  const [error, setError] = useState(null);
  const wsRef = useRef(null);
  useEffect(() => {
    setApiKey(readStoredKey());
  }, []);
  const closeSocket = (reason = "client disconnect") => {
    const ws = wsRef.current;
    if (ws && ws.readyState <= 1) {
      try {
        ws.close(1000, reason);
      } catch (_) {}
    }
    wsRef.current = null;
  };
  useEffect(() => {
    const onUnload = () => closeSocket("page unload");
    window.addEventListener("beforeunload", onUnload);
    return () => {
      window.removeEventListener("beforeunload", onUnload);
      closeSocket("component unmount");
    };
  }, []);
  const connect = () => {
    if (!apiKey) {
      setError("Paste an API key first.");
      return;
    }
    closeSocket("reconnect");
    setMessages([]);
    setError(null);
    setStatus("connecting");
    persistKey(apiKey);
    const params = new URLSearchParams();
    for (const [k, v] of Object.entries(filters)) {
      if (v != null && String(v).trim() !== "") params.set(k, String(v).trim());
    }
    params.set("token", apiKey);
    const url = `wss://${host}${path}?${params.toString()}`;
    let ws;
    try {
      ws = new WebSocket(url);
    } catch (e) {
      setStatus("error");
      setError(String(e?.message || e));
      return;
    }
    wsRef.current = ws;
    ws.onopen = () => setStatus("open");
    ws.onmessage = evt => {
      let parsed;
      try {
        parsed = JSON.parse(evt.data);
      } catch {
        parsed = evt.data;
      }
      setMessages(prev => {
        const next = [...prev, {
          ts: new Date().toISOString(),
          body: parsed
        }];
        return next.length > MAX_MESSAGES ? next.slice(-MAX_MESSAGES) : next;
      });
    };
    ws.onerror = () => {
      setStatus("error");
      setError("WebSocket error (see DevTools console for detail).");
    };
    ws.onclose = evt => {
      setStatus("closed");
      if (!evt.wasClean) {
        setError(`closed unexpectedly: code=${evt.code} reason=${evt.reason || "n/a"}`);
      }
    };
  };
  const disconnect = () => {
    closeSocket("user clicked disconnect");
    setStatus("closed");
  };
  const setFilter = (k, v) => setFilters(prev => ({
    ...prev,
    [k]: v
  }));
  const statusColor = status === "open" ? "#16a34a" : status === "error" ? "#dc2626" : "#6b7280";
  return <div style={{
    border: "1px solid #e5e7eb",
    borderRadius: 8,
    padding: 16,
    marginTop: 16,
    marginBottom: 16,
    fontFamily: "ui-sans-serif, system-ui, sans-serif"
  }}>
      <div style={{
    display: "flex",
    alignItems: "center",
    gap: 12,
    marginBottom: 12
  }}>
        <strong>Live WebSocket playground</strong>
        <span style={{
    fontSize: 12,
    color: statusColor
  }}>● {status}</span>
        <code style={{
    fontSize: 12,
    color: "#6b7280"
  }}>
          wss://{host}
          {path}
        </code>
      </div>

      {description ? <p style={{
    fontSize: 13,
    color: "#374151",
    marginTop: 0
  }}>
          {description}
        </p> : null}

      <div style={{
    display: "grid",
    gridTemplateColumns: "120px 1fr",
    gap: 8,
    alignItems: "center"
  }}>
        <label style={{
    fontSize: 12
  }}>Server</label>
        <select value={host} onChange={e => setHost(e.target.value)} disabled={status === "open" || status === "connecting"} style={{
    padding: "6px 8px",
    borderRadius: 4,
    border: "1px solid #d1d5db"
  }}>
          {SERVERS.map(s => <option key={s.host} value={s.host}>
              {s.label} — {s.host}
            </option>)}
        </select>

        <label style={{
    fontSize: 12
  }}>API key</label>
        <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} placeholder="sk_dev_… or sk_live_…" style={{
    padding: "6px 8px",
    borderRadius: 4,
    border: "1px solid #d1d5db",
    fontFamily: "ui-monospace, monospace"
  }} />

        {filterFields.flatMap(f => [<label key={`l-${f}`} style={{
    fontSize: 12
  }}>
            {f}
          </label>, <input key={`i-${f}`} type="text" value={filters[f] || ""} onChange={e => setFilter(f, e.target.value)} placeholder="optional filter" disabled={status === "open" || status === "connecting"} style={{
    padding: "6px 8px",
    borderRadius: 4,
    border: "1px solid #d1d5db",
    fontFamily: "ui-monospace, monospace"
  }} />])}
      </div>

      <div style={{
    display: "flex",
    gap: 8,
    marginTop: 12
  }}>
        <button onClick={connect} disabled={status === "open" || status === "connecting"} style={{
    background: "#0B4DA2",
    color: "white",
    border: 0,
    padding: "8px 14px",
    borderRadius: 4,
    cursor: status === "open" ? "not-allowed" : "pointer",
    opacity: status === "open" || status === "connecting" ? 0.6 : 1
  }}>
          Connect
        </button>
        <button onClick={disconnect} disabled={status !== "open" && status !== "connecting"} style={{
    background: "#fff",
    color: "#0B4DA2",
    border: "1px solid #0B4DA2",
    padding: "8px 14px",
    borderRadius: 4,
    cursor: status === "open" ? "pointer" : "not-allowed",
    opacity: status !== "open" && status !== "connecting" ? 0.6 : 1
  }}>
          Disconnect
        </button>
        <button onClick={() => setMessages([])} disabled={messages.length === 0} style={{
    background: "#fff",
    color: "#374151",
    border: "1px solid #d1d5db",
    padding: "8px 14px",
    borderRadius: 4,
    cursor: messages.length === 0 ? "not-allowed" : "pointer",
    opacity: messages.length === 0 ? 0.6 : 1
  }}>
          Clear ({messages.length})
        </button>
      </div>

      {error ? <div style={{
    marginTop: 12,
    padding: 8,
    background: "#fef2f2",
    color: "#991b1b",
    borderRadius: 4,
    fontSize: 12
  }}>
          {error}
        </div> : null}

      <div style={{
    marginTop: 12,
    background: "#0b1020",
    color: "#e5e7eb",
    borderRadius: 6,
    padding: 12,
    maxHeight: 320,
    overflow: "auto",
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    fontSize: 12,
    lineHeight: 1.5
  }}>
        {messages.length === 0 ? <div style={{
    color: "#6b7280"
  }}>
            No messages yet. Click Connect.
          </div> : messages.map((m, i) => {
    const raw = typeof m.body === "string" ? m.body : JSON.stringify(m.body, null, 2);
    const html = markAgenticKeys(escapeHtml(raw));
    return <div key={i} style={{
      marginBottom: 8,
      borderTop: i === 0 ? "none" : "1px solid #1f2937",
      paddingTop: i === 0 ? 0 : 8
    }}>
                <div style={{
      color: "#9ca3af",
      fontSize: 11
    }}>{m.ts}</div>
                <pre style={{
      margin: 0,
      whiteSpace: "pre-wrap",
      wordBreak: "break-word"
    }} dangerouslySetInnerHTML={{
      __html: html
    }} />
              </div>;
  })}
      </div>

      <div style={{
    marginTop: 8,
    fontSize: 11,
    color: "#6b7280"
  }}>
        Highlighted keys are part of the <code>meta._agentic</code> envelope.
        Last {MAX_MESSAGES} messages retained.
      </div>
    </div>;
}

<Warning>
  Your API key stays in this browser session and is attached only to the WebSocket request sent to the selected host. Pick the **Sandbox** environment while testing if you don't want to use a production key.
</Warning>

Connect to a live stream below and watch messages arrive. Highlighted keys belong to the `meta._agentic` [envelope](/develop/concepts/response-envelope). For the full stream reference, filters, and reconnect guidance, see [WebSockets](/develop/realtime/websockets).

## Try `/v2/ws/risk-events`

<WebSocketPlayground path="/v2/ws/risk-events" filterFields={["types", "severity", "subject_id", "chain", "protocol"]} description="Connect, then watch risk events stream in. Highlighted keys belong to the meta._agentic envelope." />

## Other streams

The same playground component drives every stream — change the `path` to any of `/v2/ws/yield`, `/v2/ws/yield/stats`, `/v2/ws/factors`, or `/v2/ws/factors/stats`. See [WebSockets](/develop/realtime/websockets) for what each stream carries and which filters it accepts.
