diff --git a/portal.py b/portal.py
new file mode 100644
index 0000000..9be022c
--- /dev/null
+++ b/portal.py
@@ -0,0 +1,79 @@
+"""Minimal live wizard portal: persona-aware chat over WebSocket."""
+import os, json
+from pathlib import Path
+from flask import Flask, render_template_string
+from flask_sock import Sock
+import yaml
+
+app = Flask(__name__)
+sock = Sock(app)
+ROOT = Path(os.getenv("PORTAL_ROOT", Path(__file__).resolve().parent))
+PERSONA_PATH = ROOT / "persona.yaml"
+
+with open(PERSONA_PATH, "r", encoding="utf-8") as f:
+ persona = yaml.safe_load(f) or {}
+
+PAGE = """
+
+
+
+ Timmy Time — Live Wizard Portal
+
+
+
+
+
+
+
+"""
+@app.get("/")
+def index():
+ return render_template_string(PAGE)
+
+@app.get("/health")
+def health():
+ return {"status":"ok","persona":persona.get("wizard",{}).get("name","Unknown")}
+
+@sock.route("/ws")
+def ws(ws):
+ wizard = persona.get("wizard", {})
+ catchphrases = wizard.get("catchphrases", [])
+ while True:
+ msg = ws.receive()
+ if msg is None:
+ break
+ try:
+ data = json.loads(msg)
+ except Exception:
+ continue
+ if data.get("type") == "text":
+ text = data.get("text", "").strip()
+ prefix = catchphrases[0] if catchphrases else "The mists reveal:"
+ reply = f"{prefix} {text}"
+ ws.send(json.dumps({"text": reply}))
+
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=8787)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..113afe2
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+flask
+flask-sock
+PyYAML
+requests