diff --git a/simulator/.env.example b/simulator/.env.example
new file mode 100644
index 00000000..7a87c038
--- /dev/null
+++ b/simulator/.env.example
@@ -0,0 +1,30 @@
+# ──────────────────────────────────────────────
+# MiroFish BMNR Simulator — Environment Config
+# ──────────────────────────────────────────────
+# Copy this to .env and fill in your values.
+# Only the provider you're using needs a key.
+
+# ── LLM PROVIDER ──
+# Options: anthropic, openai, gemini, openrouter, ollama, custom
+VITE_LLM_PROVIDER=anthropic
+
+# ── API KEYS (only needed for the provider you chose) ──
+VITE_ANTHROPIC_API_KEY=
+VITE_OPENAI_API_KEY=
+VITE_GEMINI_API_KEY=
+VITE_OPENROUTER_API_KEY=
+
+# ── MODEL OVERRIDE (optional — each provider has a sensible default) ──
+VITE_LLM_MODEL=
+
+# ── OLLAMA (local — no key needed) ──
+VITE_OLLAMA_BASE_URL=http://localhost:11434
+
+# ── CUSTOM PROVIDER (OpenAI-compatible endpoint) ──
+VITE_CUSTOM_BASE_URL=
+VITE_CUSTOM_API_KEY=
+
+# ── NOTE ──
+# When running inside claude.ai artifacts, no API key is needed —
+# the Anthropic API is available automatically.
+# API keys are only required when self-hosting via `npm run dev`.
diff --git a/simulator/.gitignore b/simulator/.gitignore
new file mode 100644
index 00000000..94362ebd
--- /dev/null
+++ b/simulator/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+dist/
+.env
+.env.local
+*.log
+.DS_Store
diff --git a/simulator/CONTRIBUTING.md b/simulator/CONTRIBUTING.md
new file mode 100644
index 00000000..561ea97a
--- /dev/null
+++ b/simulator/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+# Contributing to MiroFish BMNR
+
+Thanks for your interest. Here's how to help.
+
+## High-Impact Contributions
+
+These directly improve simulation accuracy:
+
+### Agent Personas
+Edit `src/data/agents.js`. The `persona` field is a natural-language prompt sent to the LLM. The richer and more specific the persona, the better the agent reasons. Think of it like briefing a method actor — give them a backstory, investment philosophy, emotional triggers, and decision-making framework.
+
+### Stimuli
+Edit `src/data/stimuli.js`. Add market events that affect BMNR. Each stimulus needs an `id`, `name`, `cat` (category), `icon`, `impact` (-1 to 1), and `desc`.
+
+### Prompt Engineering
+Edit `src/engine/prompts.js`. This constructs the prompt sent to the LLM each round. Small changes here can dramatically change output quality. Test with multiple providers — what works for Claude may not work for GPT-4o.
+
+### LLM Providers
+Edit `src/providers/index.js`. Add new providers by implementing the `call(messages, options)` interface. Return `{ text, raw }`.
+
+## Development Setup
+
+```bash
+git clone https://github.com/mikema-rgb/BMNR-Mirofish.git
+cd BMNR-Mirofish
+cp .env.example .env
+npm install
+npm run dev
+```
+
+## Pull Request Guidelines
+
+- Keep PRs focused — one feature or fix per PR
+- Test with at least 2 LLM providers if changing prompts
+- Update README if adding new features
+- Add your agent / stimulus to the appropriate data file, not to App.jsx
+
+## Code Style
+
+- No build tooling beyond Vite
+- Functional React with hooks, no class components
+- Inline styles using the `T` design token object (Wedge system)
+- Keep App.jsx as a working monolith — it needs to run as a Claude artifact
diff --git a/simulator/LICENSE b/simulator/LICENSE
new file mode 100644
index 00000000..006063e9
--- /dev/null
+++ b/simulator/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 MiroFish BMNR Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/simulator/README.md b/simulator/README.md
new file mode 100644
index 00000000..63d5984b
--- /dev/null
+++ b/simulator/README.md
@@ -0,0 +1,225 @@
+# MiroFish × BMNR Simulator
+
+**Agent-based stock simulator for BitMine (BMNR) powered by LLM swarm intelligence.**
+
+A fork of [MiroFish](https://github.com/666ghj/MiroFish) adapted for financial market simulation. Instead of predicting outcomes with formulas, 21 AI agents with distinct personas reason collectively about BMNR's future — and predictions emerge from their interactions.
+
+> **Not financial advice.** This is an educational simulation tool. Not affiliated with BitMine Immersion Technologies, Tom Lee, ARK Invest, or any entity mentioned. Agent personas are fictional archetypes.
+
+---
+
+## What It Does
+
+| Feature | Description |
+|---------|-------------|
+| **Live Market Data** | Fetches BMNR price, ETH price, mNAV, holdings from bitminetracker.io on every load |
+| **21 AI Agents** | ARK/Cathie, Tom Lee, short sellers, Reddit bulls, WSB degens, arb bots — each with detailed personas |
+| **LLM Reasoning** | Each quarter, agents reason via LLM about the market state — not programmed formulas |
+| **25 Market Stimuli** | Toggle events (ETH rally, SEC crackdown, MAVAN launch) with adjustable intensity |
+| **Flywheel Math** | Real ATM issuance mechanics: NAV calc → breakeven check → share issuance → ETH/share accretion |
+| **3 Scenarios** | Bear / Base / Bull with overlaid comparison charts |
+| **Multi-LLM** | Pluggable providers: Anthropic, OpenAI, Google Gemini, OpenRouter, Ollama (local), or custom |
+| **Formula Fallback** | Instant results via network-propagation math when LLM is unavailable |
+
+---
+
+## Quick Start
+
+### Option 1: Claude.ai Artifact (zero setup)
+
+Copy the contents of `src/App.jsx` into a Claude.ai artifact. It runs immediately — the Anthropic API is available inside artifacts with no API key.
+
+### Option 2: Self-Host
+
+```bash
+git clone https://github.com/mikema-rgb/BMNR-Mirofish.git
+cd BMNR-Mirofish
+cp .env.example .env # Edit with your API key
+npm install
+npm run dev # Opens at http://localhost:3000
+```
+
+### Option 3: Any LLM
+
+Edit `.env` to use your preferred provider:
+
+```env
+# OpenAI
+VITE_LLM_PROVIDER=openai
+VITE_OPENAI_API_KEY=sk-...
+
+# Google Gemini
+VITE_LLM_PROVIDER=gemini
+VITE_GEMINI_API_KEY=AIza...
+
+# Ollama (local, free, no key)
+VITE_LLM_PROVIDER=ollama
+VITE_OLLAMA_BASE_URL=http://localhost:11434
+
+# OpenRouter (100+ models)
+VITE_LLM_PROVIDER=openrouter
+VITE_OPENROUTER_API_KEY=sk-or-...
+
+# Any OpenAI-compatible endpoint
+VITE_LLM_PROVIDER=custom
+VITE_CUSTOM_BASE_URL=https://your-endpoint.com
+VITE_CUSTOM_API_KEY=your-key
+```
+
+---
+
+## Architecture
+
+```
+BMNR-Mirofish/
+├── src/
+│ ├── App.jsx # Main component (monolith — runs standalone)
+│ ├── main.jsx # React entry point
+│ ├── config/
+│ │ └── index.js # Simulation config, scenario metadata, fallback data
+│ ├── data/
+│ │ ├── agents.js # 21 agent personas (the core of MiroFish)
+│ │ └── stimuli.js # 25 market event definitions
+│ ├── engine/
+│ │ └── prompts.js # LLM prompt builder (fork this for other stocks)
+│ └── providers/
+│ └── index.js # LLM provider abstraction (Anthropic, OpenAI, etc.)
+├── .env.example # Environment config template
+├── package.json
+├── vite.config.js
+└── README.md
+```
+
+### How MiroFish Works (vs. Traditional Models)
+
+| | Traditional Monte Carlo | Formula Agent Model | **MiroFish LLM Agents** |
+|---|---|---|---|
+| How price moves | `price × exp(drift + vol × dW)` | Sentiment formulas drive returns | LLM reasons about what agents would do |
+| Agent behavior | None — just noise terms | Programmed bias + susceptibility | Natural language persona → LLM reasoning |
+| Emergent effects | None | Limited network propagation | Herding, contrarianism, cascade failures |
+| Stimulus response | Hardcoded impact numbers | Type-weighted sensitivity | LLM interprets event for each persona |
+| Surprise factor | None — deterministic given seed | Low — formulas are predictable | High — LLM reasoning can produce unexpected cascades |
+
+### The Simulation Loop
+
+Each quarter:
+
+1. **Agent Selection** — 8 core agents + 3 rotating guests are selected
+2. **Prompt Construction** — Market state, stimuli, prior quarter narrative, and agent personas are assembled into a prompt
+3. **LLM Call** — The prompt is sent to the configured LLM provider
+4. **Response Parsing** — JSON output is extracted with markdown fence stripping and brace-counting fallback
+5. **Market Update** — LLM-derived ETH price change and mNAV change are applied (clamped to ±50% per quarter)
+6. **Flywheel Engine** — If mNAV > breakeven, ATM shares are issued, ETH is purchased, ETH/share updates
+7. **State Propagation** — The quarter narrative feeds into the next round as context
+
+---
+
+## How to Extend
+
+### Add a New Agent
+
+Edit `src/data/agents.js`:
+
+```javascript
+{
+ id: "your_agent",
+ name: "Your Agent Name",
+ type: "Institutional", // or Retail, Quant, Macro, Analyst, etc.
+ icon: "🎯",
+ bias: 0.2, // -1 (bearish) to 1 (bullish) for formula fallback
+ influence: 0.5, // 0-1: how much they sway others
+ susceptibility: 0.3, // 0-1: how much they follow the herd
+ memory: 0.6, // 0-1: how much past rounds carry forward
+ persona: "You are... [detailed natural language persona]. You believe... You react to..."
+}
+```
+
+The persona is the most important field — write it like you're briefing a method actor.
+
+### Add a New Stimulus
+
+Edit `src/data/stimuli.js`:
+
+```javascript
+{
+ id: "your_event",
+ name: "Your Event Name",
+ cat: "ETH", // Category for filtering
+ icon: "⚡",
+ impact: 0.5, // -1 to 1 for formula fallback
+ desc: "Short description"
+}
+```
+
+### Add a New LLM Provider
+
+Edit `src/providers/index.js`:
+
+```javascript
+export const YourProvider = {
+ id: "your_provider",
+ name: "Your Provider",
+ models: ["model-1", "model-2"],
+ defaultModel: "model-1",
+ requiresKey: true,
+
+ async call(messages, { model, maxTokens, apiKey } = {}) {
+ // Make your API call here
+ // Return { text: string, raw: any }
+ const res = await fetch("https://your-api.com/v1/chat", { ... });
+ const data = await res.json();
+ return { text: data.output, raw: data };
+ }
+};
+
+// Register it:
+export const PROVIDERS = {
+ ...existingProviders,
+ your_provider: YourProvider,
+};
+```
+
+### Fork for Another Stock
+
+The simulator is designed to be adapted. To fork for a different stock:
+
+1. Replace agent personas in `src/data/agents.js` with archetypes relevant to your stock
+2. Replace stimuli in `src/data/stimuli.js` with events that affect your stock
+3. Update `src/config/index.js` with fallback market data for your stock
+4. Modify `src/engine/prompts.js` to reference your stock's specific dynamics (the flywheel mechanics are BMNR-specific — replace with whatever drives your stock's value)
+5. Update the live data fetcher in `src/App.jsx` to pull from relevant data sources
+
+---
+
+## Acknowledgments
+
+- **[MiroFish](https://github.com/666ghj/MiroFish)** — the original swarm intelligence engine by Shanda Group. This project adapts their agent-based simulation concept for financial markets.
+- **[OASIS](https://github.com/camel-ai/oasis)** — the underlying social simulation platform that powers MiroFish.
+- **[Wedge](https://wedge.so)** — the BMNR flywheel model and design system.
+- **[bitminetracker.io](https://bitminetracker.io)** — live BMNR market data.
+
+---
+
+## Contributing
+
+PRs welcome. The most impactful contributions:
+
+- **Better agent personas** — if you can write a more realistic persona for any of the 21 agents, that directly improves simulation quality
+- **New stimuli** — market events we haven't thought of
+- **New LLM providers** — expand the provider abstraction
+- **Prompt engineering** — improvements to `src/engine/prompts.js` that produce more realistic agent reasoning
+- **UI improvements** — better charts, mobile responsiveness, dark mode
+- **Other stocks** — fork and adapt for MSTR, COIN, or any other stock with similar dynamics
+
+---
+
+## License
+
+MIT — same as MiroFish.
+
+---
+
+
+ WEDGE × MIROFISH × BMNR
+ Educational only. Not financial advice.
+
diff --git a/simulator/index.html b/simulator/index.html
new file mode 100644
index 00000000..9fed8049
--- /dev/null
+++ b/simulator/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ MiroFish × BMNR Simulator
+
+
+
+
+
+
diff --git a/simulator/package.json b/simulator/package.json
new file mode 100644
index 00000000..4ad4f600
--- /dev/null
+++ b/simulator/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "BMNR-Mirofish",
+ "version": "1.0.0",
+ "description": "MiroFish-powered agent-based stock simulator for BitMine (BMNR). Uses LLM-driven swarm intelligence to predict market outcomes.",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "recharts": "^2.13.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.0",
+ "vite": "^5.4.0"
+ },
+ "keywords": [
+ "bmnr", "bitmine", "ethereum", "mirofish", "agent-based",
+ "simulation", "llm", "swarm-intelligence", "stock-simulator",
+ "flywheel", "mnav", "crypto-treasury"
+ ],
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/mikema-rgb/BMNR-Mirofish"
+ }
+}
diff --git a/simulator/src/App.jsx b/simulator/src/App.jsx
new file mode 100644
index 00000000..81f3ef48
--- /dev/null
+++ b/simulator/src/App.jsx
@@ -0,0 +1,787 @@
+import React, { useState, useMemo, useCallback, useEffect, useRef } from "react";
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Area, AreaChart, ComposedChart, Bar, Cell } from "recharts";
+
+/* ═══ DESIGN TOKENS — Wedge Day Mode ═══ */
+const T = {
+ bg: "#f7f8fa", bg2: "#eef0f3", white: "#ffffff",
+ green: "#007a3d", greenDim: "rgba(0,122,61,0.08)", greenBorder: "rgba(0,122,61,0.18)",
+ blue: "#005f99", blueDim: "rgba(0,95,153,0.08)", blueBorder: "rgba(0,95,153,0.18)",
+ text: "#0a0d11", textDim: "#5a6478", textLight: "#8a94a6",
+ border: "rgba(0,0,0,0.08)", borderMed: "rgba(0,0,0,0.12)",
+ red: "#c84b31", redDim: "rgba(200,75,49,0.06)", redBorder: "rgba(200,75,49,0.18)",
+ gold: "#b8941e", goldDim: "rgba(184,148,30,0.08)", goldBorder: "rgba(184,148,30,0.18)",
+ purple: "#6b4fa0", purpleDim: "rgba(107,79,160,0.08)", purpleBorder: "rgba(107,79,160,0.18)",
+ mono: "'IBM Plex Mono', monospace", sans: "'Barlow', sans-serif", cond: "'Barlow Condensed', sans-serif",
+};
+
+/* ═══ FORMATTERS ═══ */
+function fmt(n, d = 2) { if (!isFinite(n)) return "—"; if (Math.abs(n) >= 1e12) return `$${(n/1e12).toFixed(d)}T`; if (Math.abs(n) >= 1e9) return `$${(n/1e9).toFixed(d)}B`; if (Math.abs(n) >= 1e6) return `$${(n/1e6).toFixed(d)}M`; if (Math.abs(n) >= 1e3) return `$${(n/1e3).toFixed(d)}K`; return `$${n.toFixed(d)}`; }
+function fN(n) { if (!isFinite(n)) return "—"; if (Math.abs(n) >= 1e6) return (n/1e6).toFixed(2)+"M"; if (Math.abs(n) >= 1e3) return (n/1e3).toFixed(1)+"K"; return n.toFixed(0); }
+function pct(n) { if (!isFinite(n)) return "—"; return `${n>=0?"+":""}${(n*100).toFixed(2)}%`; }
+function safe(a, b) { return b === 0 ? 0 : a / b; }
+
+/* ═══ LIVE DATA — Fallback + API Fetch ═══ */
+const FALLBACK = {
+ price: 23.37, ethPrice: 2314.60, ethBalance: 4595563, shares: 530621703, nav: 22.57, mNAV: 1.04,
+ staked: 3040483, avgCost: 3753.88, cash: 1.2e9, beast: 0.2e9, btcVal: 196*85000,
+ analystLo: 30, analystHi: 39, w52Lo: 3.20, w52Hi: 161, fetchedAt: null, isLive: false,
+};
+async function fetchLiveBMNR() {
+ try {
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
+ method: "POST", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ model: "claude-sonnet-4-20250514", max_tokens: 1000,
+ tools: [{ type: "web_search_20250305", name: "web_search" }],
+ messages: [{ role: "user", content: `Search for current BMNR stock data from bitminetracker.io. Return ONLY a JSON object: {"price":,"ethPrice":,"ethBalance":,"shares":,"nav":,"mNAV":,"staked":,"avgCost":,"cash":,"beast":,"btcVal":,"analystLo":,"analystHi":,"w52Lo":,"w52Hi":}` }],
+ }),
+ });
+ if (!res.ok) return null;
+ const data = await res.json();
+ const txt = (data.content||[]).filter(b=>b.type==="text").map(b=>b.text).join("\n");
+ const m = txt.match(/\{[^{}]*"price"\s*:\s*[\d.]+[^{}]*\}/);
+ if (!m) return null;
+ let p; try { p = JSON.parse(m[0]); } catch { return null; }
+ if (typeof p.price !== "number" || p.price <= 0) return null;
+ return { ...FALLBACK, ...p, fetchedAt: new Date().toLocaleTimeString(), isLive: true };
+ } catch { return null; }
+}
+
+/* ═══════════════════════════════════════════════════
+ AGENT PERSONAS — MiroFish-style detailed personas
+ Each agent has a natural-language persona that drives
+ LLM reasoning (not just a numeric bias)
+═══════════════════════════════════════════════════ */
+const AGENTS = [
+ { id: "ark", name: "ARK / Cathie Wood", type: "Institutional", icon: "🏛️", persona: "You are Cathie Wood's ARK Invest. You are a conviction buyer of disruptive technology. You see BitMine as a leveraged play on Ethereum's transformative potential. You hold millions of BMNR shares and buy dips aggressively. You believe ETH will reach $10K+ and BMNR is massively undervalued.", bias: 0.5, influence: 0.85, susceptibility: 0.15, memory: 0.7 },
+ { id: "fidelity", name: "Fidelity Digital", type: "Institutional", icon: "🏦", persona: "You are a Fidelity institutional allocator. You evaluate BMNR on fundamentals: NAV discount/premium, cash flow from staking, dilution risk. You are cautiously optimistic but need to see MAVAN revenue materialize before increasing position.", bias: 0.2, influence: 0.8, susceptibility: 0.1, memory: 0.8 },
+ { id: "mozayyx", name: "MOZAYYX Fund", type: "Institutional", icon: "💎", persona: "You are MOZAYYX, a crypto-focused fund with deep conviction in BitMine's Alchemy of 5% strategy. You see the ETH accumulation as a generational opportunity. You rarely sell and add on weakness.", bias: 0.4, influence: 0.7, susceptibility: 0.2, memory: 0.6 },
+ { id: "short_seller", name: "Short Seller Report", type: "Institutional", icon: "🐻", persona: "You are an activist short seller. You believe BitMine is a promotion: negative earnings, massive dilution, stock price entirely dependent on ETH price. The 100x share authorization is a red flag. mNAV premium is unjustified. You publish bearish reports.", bias: -0.6, influence: 0.65, susceptibility: 0.2, memory: 0.5 },
+ { id: "pension", name: "Pension Allocator", type: "Institutional", icon: "📊", persona: "You are a conservative pension fund. You are skeptical of crypto treasury companies. You need to see stable cash flows, not speculative ETH price appreciation. Dilution concerns dominate your analysis.", bias: -0.1, influence: 0.6, susceptibility: 0.25, memory: 0.9 },
+ { id: "tom_lee", name: "Tom Lee (CEO)", type: "Insider", icon: "👔", persona: "You are Tom Lee, CEO of BitMine. You are the architect of the Alchemy of 5% strategy. You believe ETH is severely undervalued and BMNR will be a $100+ stock. You use ATM offerings strategically to accumulate ETH. You dismiss short-seller criticism as lacking vision.", bias: 0.7, influence: 0.9, susceptibility: 0.05, memory: 0.3 },
+ { id: "analyst", name: "B. Riley Analyst", type: "Analyst", icon: "📝", persona: "You are a sell-side equity analyst covering BMNR with a price target of $30-39. You focus on mNAV, ETH/share accretion, MAVAN staking revenue potential, and dilution math. You are moderately bullish but flag execution risk.", bias: 0.3, influence: 0.7, susceptibility: 0.15, memory: 0.7 },
+ { id: "ct_analyst", name: "Crypto Twitter Analyst", type: "Analyst", icon: "🧵", persona: "You are a crypto-native analyst on X/Twitter. You view BMNR as the best ETH proxy in equities. You track on-chain ETH flows, staking yields, and mNAV daily. You're bullish but aware of the dilution treadmill.", bias: 0.2, influence: 0.55, susceptibility: 0.4, memory: 0.4 },
+ { id: "bear_writer", name: "Seeking Alpha Bear", type: "Analyst", icon: "✍️", persona: "You write bearish analysis on Seeking Alpha. You believe BMNR's premium to NAV is irrational, the share dilution is destroying value, and the company has no real revenue. You compare it unfavorably to simply buying ETH directly.", bias: -0.5, influence: 0.45, susceptibility: 0.3, memory: 0.6 },
+ { id: "cnbc", name: "CNBC / Cramer", type: "Media", icon: "📺", persona: "You are a financial media personality. You react to price action and headlines. You swing between excitement when BMNR rallies and caution when it drops. You amplify whatever the current narrative is.", bias: 0.0, influence: 0.6, susceptibility: 0.5, memory: 0.2 },
+ { id: "reddit_bull", name: "r/BMNR Bull", type: "Retail", icon: "🦍", persona: "You are a passionate BMNR retail investor on Reddit. You believe in the thesis long-term and buy every dip. You dismiss bears as shorts who 'don't get it'. You post rocket emojis and hold through drawdowns.", bias: 0.4, influence: 0.3, susceptibility: 0.6, memory: 0.3 },
+ { id: "reddit_skeptic", name: "r/BMNR Skeptic", type: "Retail", icon: "🤔", persona: "You are a skeptical retail investor. You hold some BMNR but worry about dilution and the gap between stock price and NAV. You ask tough questions and demand clarity on the ATM program.", bias: -0.2, influence: 0.25, susceptibility: 0.5, memory: 0.5 },
+ { id: "wsb", name: "WSB Degen", type: "Retail", icon: "🎰", persona: "You are a WallStreetBets trader. You trade BMNR options for volatility. You buy calls before catalysts and puts after euphoria. You have no long-term thesis, only momentum. You follow whatever is trending.", bias: 0.1, influence: 0.35, susceptibility: 0.8, memory: 0.1 },
+ { id: "eth_maxi", name: "ETH Maximalist", type: "Retail", icon: "⟠", persona: "You are an Ethereum maximalist. You think owning BMNR is an inefficient way to get ETH exposure with added dilution risk. However, you acknowledge the leveraged upside if ETH moons. You prefer holding ETH directly.", bias: 0.0, influence: 0.4, susceptibility: 0.3, memory: 0.4 },
+ { id: "value", name: "Value Investor", type: "Retail", icon: "📐", persona: "You are a Graham-style value investor. You only buy BMNR below NAV (mNAV < 1.0). You think the current premium is speculative. You would be a buyer at $15-18 (discount to NAV) but not at current levels.", bias: -0.3, influence: 0.35, susceptibility: 0.2, memory: 0.8 },
+ { id: "swing", name: "Swing Trader", type: "Retail", icon: "📉", persona: "You are a pure technical trader. You only care about chart patterns, volume, and support/resistance levels. Fundamentals are irrelevant to you. You trade the falling wedge pattern and key levels around $20-22.", bias: 0.0, influence: 0.2, susceptibility: 0.7, memory: 0.15 },
+ { id: "mm", name: "Market Maker", type: "Quant", icon: "🤖", persona: "You are an automated market maker. You provide liquidity and profit from the bid-ask spread. You are always delta neutral. You observe order flow imbalances to gauge short-term direction.", bias: 0.0, influence: 0.5, susceptibility: 0.0, memory: 0.0 },
+ { id: "momentum", name: "Momentum Algo", type: "Quant", icon: "⚡", persona: "You are a trend-following algorithm. You buy when price is above its moving averages with increasing volume. You sell when momentum fades. You have no opinion on fundamentals, only price action.", bias: 0.0, influence: 0.4, susceptibility: 0.0, memory: 0.95 },
+ { id: "arb", name: "mNAV Arb Bot", type: "Quant", icon: "🔄", persona: "You are an arbitrage algorithm that trades the mNAV premium/discount. When mNAV > 1.3, you short BMNR and buy ETH. When mNAV < 0.8, you buy BMNR and short ETH. You push mNAV toward fair value.", bias: 0.0, influence: 0.45, susceptibility: 0.0, memory: 0.0 },
+ { id: "macro_bull", name: "Macro Bull", type: "Macro", icon: "🌊", persona: "You are a macro strategist who believes we're entering a liquidity-driven bull market. Fed rate cuts, weakening dollar, and crypto adoption create tailwinds for BMNR. Risk-on environments favor leveraged crypto plays.", bias: 0.2, influence: 0.5, susceptibility: 0.3, memory: 0.6 },
+ { id: "macro_bear", name: "Macro Bear", type: "Macro", icon: "🏔️", persona: "You are a macro strategist warning about recession risk, sticky inflation, and higher-for-longer rates. Risk assets including crypto are vulnerable. BMNR's leverage to ETH magnifies downside in a risk-off environment.", bias: -0.3, influence: 0.5, susceptibility: 0.3, memory: 0.6 },
+];
+
+/* ═══ STIMULI CATALOG ═══ */
+const STIMULI = [
+ { id: "eth_5k", name: "ETH Breaks $5,000", cat: "ETH", icon: "🚀", impact: 0.7, desc: "Institutional adoption wave" },
+ { id: "eth_crash", name: "ETH Crashes < $800", cat: "ETH", icon: "💀", impact: -0.8, desc: "Crypto winter 3.0" },
+ { id: "eth_etf_in", name: "ETH ETF Mega-Inflows", cat: "ETH", icon: "📈", impact: 0.5, desc: "$10B+ spot ETF flows" },
+ { id: "eth_etf_out", name: "ETH ETF Redemptions", cat: "ETH", icon: "📤", impact: -0.45, desc: "Institutions dump positions" },
+ { id: "eth_upgrade", name: "Pectra Upgrade", cat: "ETH", icon: "⬆️", impact: 0.3, desc: "Protocol upgrade succeeds" },
+ { id: "mavan", name: "MAVAN Staking Live", cat: "BitMine", icon: "⚡", impact: 0.6, desc: "$330M+ annual staking rev" },
+ { id: "mavan_delay", name: "MAVAN Delayed", cat: "BitMine", icon: "⏳", impact: -0.35, desc: "Technical issues" },
+ { id: "alchemy5", name: "Alchemy 5% Hit", cat: "BitMine", icon: "🧪", impact: 0.65, desc: "5% of all ETH supply" },
+ { id: "beast_ipo", name: "Beast Industries IPO", cat: "BitMine", icon: "🎬", impact: 0.4, desc: "$200M stake monetized" },
+ { id: "scandal", name: "Executive Scandal", cat: "BitMine", icon: "⚠️", impact: -0.6, desc: "Accounting concerns" },
+ { id: "sp500", name: "Index Inclusion", cat: "BitMine", icon: "🏛️", impact: 0.45, desc: "S&P 500 / Russell add" },
+ { id: "dilution", name: "100x Auth Used", cat: "Corporate", icon: "💧", impact: -0.55, desc: "Massive dilution" },
+ { id: "buyback", name: "Buyback $500M", cat: "Corporate", icon: "🔄", impact: 0.35, desc: "Aggressive buyback" },
+ { id: "convert", name: "Convertible $2B", cat: "Corporate", icon: "📜", impact: -0.2, desc: "Debt for ETH buys" },
+ { id: "fed_cut", name: "Fed Cuts 150bps", cat: "Macro", icon: "📉", impact: 0.4, desc: "Aggressive easing" },
+ { id: "recession", name: "US Recession", cat: "Macro", icon: "🏚️", impact: -0.5, desc: "Risk-off everywhere" },
+ { id: "pro_crypto", name: "Pro-Crypto Law", cat: "Macro", icon: "⚖️", impact: 0.4, desc: "Regulatory clarity" },
+ { id: "sec", name: "SEC Crackdown", cat: "Macro", icon: "🚫", impact: -0.6, desc: "Investment co. risk" },
+ { id: "btc_150k", name: "BTC $150K", cat: "Macro", icon: "₿", impact: 0.45, desc: "BTC supercycle" },
+ { id: "squeeze", name: "Short Squeeze", cat: "Technical", icon: "🔥", impact: 0.6, desc: "30%+ SI unwind" },
+ { id: "ark_exit", name: "ARK Sells All", cat: "Technical", icon: "🚪", impact: -0.5, desc: "Cathie exits" },
+ { id: "rival", name: "Rival Treasury", cat: "Technical", icon: "🏁", impact: -0.25, desc: "Major competitor" },
+ { id: "viral", name: "Viral Social Pump", cat: "Social", icon: "📱", impact: 0.3, desc: "BMNR trends on X" },
+ { id: "fud", name: "Coordinated FUD", cat: "Social", icon: "🗞️", impact: -0.35, desc: "Short-seller report" },
+ { id: "openai", name: "OpenAI/Eightco Win", cat: "Social", icon: "🤖", impact: 0.35, desc: "$80M bet pays off" },
+];
+
+const SIM_ROUNDS = 8; // 8 quarterly rounds = 2 years
+const SCENARIO_META = {
+ bear: { label: "BEAR", color: T.red, desc: "ETH collapses, dilution spiral, premium vanishes" },
+ base: { label: "BASE", color: T.blue, desc: "ETH $2-3K, MAVAN launches, moderate growth" },
+ bull: { label: "BULL", color: T.green, desc: "ETH breaks ATH, Alchemy 5% hit, staking revenue" },
+};
+
+/* ═══════════════════════════════════════════════════
+ LLM-DRIVEN SIMULATION ENGINE (MiroFish-style)
+
+ Each round: all 21 agents reason via a single Anthropic
+ API call. The LLM evaluates each agent's persona against
+ the current market state, stimuli, and prior round context.
+ Returns per-agent sentiment, reasoning, and action.
+
+ This is the core MiroFish insight: predictions emerge
+ from collective LLM reasoning, not programmed formulas.
+═══════════════════════════════════════════════════ */
+async function runLLMRound(roundNum, marketState, activeStimuli, prevRoundSummary, scenarioMode) {
+ const stimDesc = activeStimuli.map(s => {
+ const st = STIMULI.find(x => x.id === s.id);
+ return st ? `${st.name} (${(s.intensity||1).toFixed(1)}x) — ${st.desc}` : "";
+ }).filter(Boolean).join("; ");
+
+ // Pick 8 most important agents per round (rotate + always include key archetypes)
+ const coreIds = ["tom_lee", "ark", "short_seller", "analyst", "reddit_bull", "arb", "macro_bull", "macro_bear"];
+ const rotatingIds = AGENTS.filter(a => !coreIds.includes(a.id)).map(a => a.id);
+ const rotateStart = ((roundNum - 1) * 3) % rotatingIds.length;
+ const extraIds = [rotatingIds[rotateStart % rotatingIds.length], rotatingIds[(rotateStart + 1) % rotatingIds.length], rotatingIds[(rotateStart + 2) % rotatingIds.length]];
+ const roundAgentIds = [...new Set([...coreIds, ...extraIds])];
+ const roundAgents = AGENTS.filter(a => roundAgentIds.includes(a.id));
+
+ const agentList = roundAgents.map(a =>
+ `- ${a.name} [id:${a.id}] (${a.type}): ${a.persona.split(".").slice(0, 2).join(".")}.`
+ ).join("\n");
+
+ const userMsg = `SCENARIO: ${scenarioMode.toUpperCase()} CASE | QUARTER: Q${roundNum} of ${SIM_ROUNDS}
+
+MARKET STATE:
+BMNR: $${marketState.stockPrice.toFixed(2)} | ETH: $${marketState.ethPrice.toFixed(0)} | mNAV: ${marketState.mNAV.toFixed(2)}x | NAV/Shr: $${marketState.navPerShare.toFixed(2)} | ETH/Shr: ${marketState.ethPerShare.toFixed(6)} | Holdings: ${fN(marketState.ethHoldings)} ETH (${(marketState.ethHoldings / 120e6 * 100).toFixed(1)}% supply) | Breakeven: ${marketState.breakevenMNav.toFixed(2)}x | Flywheel: ${marketState.mNAV > marketState.breakevenMNav ? "ACCRETIVE" : "DILUTIVE"}
+
+${stimDesc ? `EVENTS: ${stimDesc}` : "NO EVENTS"}
+${prevRoundSummary ? `PREV QUARTER: ${prevRoundSummary}` : "First quarter."}
+
+AGENTS:
+${agentList}
+
+For the ${scenarioMode.toUpperCase()} case, determine each agent's reaction. Lean ${scenarioMode === "bear" ? "bearish — things go wrong" : scenarioMode === "bull" ? "bullish — catalysts hit" : "moderate — mixed signals"}. Think about herding, contrarianism, reflexivity.`;
+
+ try {
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ model: "claude-sonnet-4-20250514",
+ max_tokens: 4096,
+ messages: [{
+ role: "user",
+ content: `You are a financial market simulation engine. You MUST respond with ONLY a raw JSON object. No markdown fences, no backticks, no explanation before or after the JSON. Just the raw JSON starting with { and ending with }.
+
+${userMsg}
+
+Respond with ONLY this JSON structure (no other text):
+{"agents":[{"id":"agent_id","sentiment":-1.0 to 1.0,"action":"BUY or SELL or HOLD","reasoning":"1 sentence","priceTarget":30}],"ethPriceChange":0.05,"mNavChange":0.02,"quarterSummary":"2 sentences"}`
+ }],
+ }),
+ });
+
+ if (!res.ok) {
+ const errText = await res.text().catch(() => "");
+ console.error(`LLM round ${roundNum} HTTP ${res.status}:`, errText.slice(0, 200));
+ return null;
+ }
+
+ const data = await res.json();
+ const txt = (data.content || []).filter(b => b.type === "text").map(b => b.text).join("\n");
+
+ if (!txt || txt.length < 10) {
+ console.error(`LLM round ${roundNum}: empty response`);
+ return null;
+ }
+
+ // Strip markdown fences if present
+ let cleaned = txt.replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
+
+ // Try direct parse first (ideal case: model returned pure JSON)
+ let parsed = null;
+ try { parsed = JSON.parse(cleaned); } catch {}
+
+ // Fallback: extract the outermost JSON object containing "agents"
+ if (!parsed) {
+ // Find the opening { before "agents" and match to its closing }
+ const agentsIdx = cleaned.indexOf('"agents"');
+ if (agentsIdx === -1) { console.error(`LLM round ${roundNum}: no "agents" in response`); return null; }
+
+ // Walk backward to find the opening brace
+ let start = cleaned.lastIndexOf("{", agentsIdx);
+ if (start === -1) { console.error(`LLM round ${roundNum}: no opening brace`); return null; }
+
+ // Walk forward from start, counting braces to find the matching close
+ let depth = 0;
+ let end = -1;
+ for (let i = start; i < cleaned.length; i++) {
+ if (cleaned[i] === "{") depth++;
+ else if (cleaned[i] === "}") { depth--; if (depth === 0) { end = i; break; } }
+ }
+ if (end === -1) { console.error(`LLM round ${roundNum}: unbalanced braces`); return null; }
+
+ try { parsed = JSON.parse(cleaned.slice(start, end + 1)); } catch (e) {
+ console.error(`LLM round ${roundNum}: JSON parse failed:`, e.message, cleaned.slice(start, start + 100));
+ return null;
+ }
+ }
+
+ if (!parsed || !Array.isArray(parsed.agents) || parsed.agents.length === 0) {
+ console.error(`LLM round ${roundNum}: invalid structure`, JSON.stringify(parsed).slice(0, 100));
+ return null;
+ }
+
+ // Fill in agents that weren't in this round's LLM call
+ const fullAgents = AGENTS.map(a => {
+ const llmA = parsed.agents.find(la => la.id === a.id);
+ if (llmA) return llmA;
+ // Agents not in this round: carry forward a neutral estimate based on bias
+ return { id: a.id, sentiment: a.bias * 0.5, action: "HOLD", reasoning: "Not polled this quarter.", priceTarget: null };
+ });
+ parsed.agents = fullAgents;
+
+ console.log(`LLM round ${roundNum} OK: ${parsed.agents.length} agents, ethΔ=${parsed.ethPriceChange}, summary=${(parsed.quarterSummary||"").slice(0, 50)}`);
+ return parsed;
+ } catch (e) {
+ console.error(`LLM round ${roundNum} exception:`, e);
+ return null;
+ }
+}
+
+/* ═══ FORMULA ENGINE (fast fallback) ═══ */
+function runFormulaSimulation(activeStimuli, scenarioMode, live) {
+ const scenarioMul = { bear: -0.4, base: 0.0, bull: 0.4 }[scenarioMode];
+ const stimPressure = {};
+ AGENTS.forEach(a => {
+ let p = 0;
+ activeStimuli.forEach(s => {
+ const st = STIMULI.find(x => x.id === s.id);
+ if (st) p += st.impact * (s.intensity || 1) * 0.5;
+ });
+ stimPressure[a.id] = p;
+ });
+
+ const agentStates = {};
+ AGENTS.forEach(a => { agentStates[a.id] = { sentiment: a.bias + scenarioMul * 0.3, history: [], reasoning: "" }; });
+
+ let ethPrice = live.ethPrice, ethHoldings = live.ethBalance, shares = live.shares;
+ let cash = live.cash, mNAV = live.mNAV;
+ const btcVal = live.btcVal, beastVal = live.beast;
+ const rounds = [];
+ let prevSent = 0;
+
+ for (let r = 0; r <= SIM_ROUNDS; r++) {
+ if (r > 0) {
+ AGENTS.forEach(a => {
+ const st = agentStates[a.id];
+ let ns = a.bias * 0.3 + scenarioMul * 0.15;
+ ns += (stimPressure[a.id] || 0) * Math.max(0.3, 1 - r * 0.05);
+ if (a.susceptibility > 0) {
+ let ni = 0, nw = 0;
+ AGENTS.forEach(o => { if (o.id !== a.id) { const w = o.influence * a.susceptibility; ni += agentStates[o.id].sentiment * w; nw += w; } });
+ if (nw > 0) ns += (ni / nw) * a.susceptibility * 0.4;
+ }
+ if (a.memory > 0 && st.history.length > 0) ns += st.history.slice(-3).reduce((s, v) => s + v, 0) / Math.min(3, st.history.length) * a.memory * 0.2;
+ if (a.id === "arb") ns = -(mNAV - 1.0) * 0.5;
+ if (a.id === "momentum") ns = prevSent * 0.8;
+ ns += (Math.random() - 0.5) * 0.15;
+ st.sentiment = Math.max(-1, Math.min(1, ns));
+ st.history.push(st.sentiment);
+ st.reasoning = st.sentiment > 0.2 ? "Bullish on current setup" : st.sentiment < -0.2 ? "Concerned about downside risks" : "Watching from the sidelines";
+ });
+ }
+
+ let aggS = 0, aggW = 0;
+ AGENTS.forEach(a => { aggS += agentStates[a.id].sentiment * a.influence; aggW += a.influence; });
+ aggS = aggW > 0 ? aggS / aggW : 0;
+ prevSent = aggS;
+
+ if (r > 0) {
+ ethPrice *= (1 + aggS * 0.08 + (Math.random() - 0.5) * 0.06);
+ ethPrice = Math.max(200, ethPrice);
+ mNAV *= (1 + aggS * 0.06 + (Math.random() - 0.5) * 0.04);
+ mNAV = Math.max(0.3, Math.min(4.0, mNAV));
+ }
+
+ const ethVal = ethHoldings * ethPrice;
+ const nav = ethVal + btcVal + beastVal + cash;
+ const navPS = safe(nav, shares);
+ const stockP = mNAV * navPS;
+ const ethPS = safe(ethHoldings, shares);
+ const bev = safe(ethVal, nav);
+
+ let issued = 0, bought = 0, raised = 0;
+ if (r > 0 && mNAV > bev && mNAV > 0.8) {
+ issued = Math.round(shares * 0.02 * Math.min(1, (mNAV - bev) / 0.3));
+ raised = issued * stockP;
+ bought = raised / ethPrice;
+ ethHoldings += bought; shares += issued;
+ }
+
+ const snap = AGENTS.map(a => ({ ...a, sentiment: agentStates[a.id].sentiment, reasoning: agentStates[a.id].reasoning }));
+ const bullish = snap.filter(a => a.sentiment > 0.1).length;
+ const bearish = snap.filter(a => a.sentiment < -0.1).length;
+
+ rounds.push({ round: r, ethPrice, mNAV, navPerShare: navPS, stockPrice: stockP, ethPerShare: ethPS,
+ ethHoldings, shares, cash: Math.max(0, cash), sharesIssued: issued, ethBought: bought,
+ capitalRaised: raised, isAccretive: mNAV > bev, breakevenMNav: bev,
+ aggSentiment: aggS, bullish, bearish, agentSnapshot: snap, quarterSummary: "", llmPowered: false });
+ }
+ return rounds;
+}
+
+/* ═══════════════════════════════════════════════════
+ FULL LLM SIMULATION (async, progressive)
+═══════════════════════════════════════════════════ */
+async function runFullLLMSimulation(activeStimuli, scenarioMode, live, onRoundComplete) {
+ let ethPrice = live.ethPrice, ethHoldings = live.ethBalance, shares = live.shares;
+ let cash = live.cash, mNAV = live.mNAV;
+ const btcVal = live.btcVal, beastVal = live.beast;
+ const rounds = [];
+ let prevSummary = "";
+
+ // Round 0: initial state
+ const ethVal0 = ethHoldings * ethPrice;
+ const nav0 = ethVal0 + btcVal + beastVal + cash;
+ const snap0 = AGENTS.map(a => ({ ...a, sentiment: a.bias, reasoning: "Awaiting first quarter data." }));
+ rounds.push({ round: 0, ethPrice, mNAV, navPerShare: safe(nav0, shares), stockPrice: mNAV * safe(nav0, shares),
+ ethPerShare: safe(ethHoldings, shares), ethHoldings, shares, cash, sharesIssued: 0, ethBought: 0,
+ capitalRaised: 0, isAccretive: true, breakevenMNav: safe(ethVal0, nav0),
+ aggSentiment: 0, bullish: AGENTS.filter(a => a.bias > 0.1).length, bearish: AGENTS.filter(a => a.bias < -0.1).length,
+ agentSnapshot: snap0, quarterSummary: "Initial state. Simulation begins.", llmPowered: true });
+ onRoundComplete([...rounds]);
+
+ for (let r = 1; r <= SIM_ROUNDS; r++) {
+ const marketState = {
+ stockPrice: rounds[r-1].stockPrice, ethPrice, mNAV,
+ navPerShare: rounds[r-1].navPerShare, ethPerShare: rounds[r-1].ethPerShare,
+ ethHoldings, breakevenMNav: rounds[r-1].breakevenMNav,
+ };
+
+ const llmResult = await runLLMRound(r, marketState, activeStimuli, prevSummary, scenarioMode);
+
+ if (llmResult && llmResult.agents) {
+ // Apply LLM-derived market changes
+ const ethChg = typeof llmResult.ethPriceChange === "number" ? llmResult.ethPriceChange : 0;
+ const mNavChg = typeof llmResult.mNavChange === "number" ? llmResult.mNavChange : 0;
+ ethPrice *= (1 + Math.max(-0.5, Math.min(0.5, ethChg)));
+ ethPrice = Math.max(200, ethPrice);
+ mNAV *= (1 + Math.max(-0.4, Math.min(0.4, mNavChg)));
+ mNAV = Math.max(0.3, Math.min(4.0, mNAV));
+ prevSummary = llmResult.quarterSummary || "";
+
+ // Build agent snapshot from LLM output
+ const snap = AGENTS.map(a => {
+ const llmAgent = llmResult.agents.find(la => la.id === a.id);
+ return {
+ ...a,
+ sentiment: llmAgent ? Math.max(-1, Math.min(1, llmAgent.sentiment || 0)) : a.bias,
+ reasoning: llmAgent?.reasoning || "No comment this quarter.",
+ action: llmAgent?.action || "HOLD",
+ priceTarget: llmAgent?.priceTarget || null,
+ };
+ });
+
+ // Aggregate sentiment
+ let aggS = 0, aggW = 0;
+ snap.forEach(a => { aggS += a.sentiment * (AGENTS.find(x=>x.id===a.id)?.influence || 0.5); aggW += (AGENTS.find(x=>x.id===a.id)?.influence || 0.5); });
+ aggS = aggW > 0 ? aggS / aggW : 0;
+
+ // Flywheel
+ const ethVal = ethHoldings * ethPrice;
+ const nav = ethVal + btcVal + beastVal + cash;
+ const navPS = safe(nav, shares);
+ const stockP = mNAV * navPS;
+ const ethPS = safe(ethHoldings, shares);
+ const bev = safe(ethVal, nav);
+
+ let issued = 0, bought = 0, raised = 0;
+ if (mNAV > bev && mNAV > 0.8) {
+ issued = Math.round(shares * 0.02 * Math.min(1, (mNAV - bev) / 0.3));
+ raised = issued * stockP; bought = raised / ethPrice;
+ ethHoldings += bought; shares += issued;
+ }
+
+ rounds.push({ round: r, ethPrice, mNAV, navPerShare: navPS, stockPrice: stockP, ethPerShare: ethPS,
+ ethHoldings, shares, cash: Math.max(0, cash), sharesIssued: issued, ethBought: bought,
+ capitalRaised: raised, isAccretive: mNAV > bev, breakevenMNav: bev,
+ aggSentiment: aggS, bullish: snap.filter(a => a.sentiment > 0.1).length,
+ bearish: snap.filter(a => a.sentiment < -0.1).length,
+ agentSnapshot: snap, quarterSummary: prevSummary, llmPowered: true });
+ } else {
+ // LLM failed — use formula fallback for this round
+ const fallbackRound = runFormulaSimulation(activeStimuli, scenarioMode, { ...live, ethPrice, ethBalance: ethHoldings, shares, cash, mNAV, btcVal, beast: beastVal });
+ const fr = fallbackRound[Math.min(r, fallbackRound.length - 1)];
+ if (fr) {
+ ethPrice = fr.ethPrice; mNAV = fr.mNAV; ethHoldings = fr.ethHoldings; shares = fr.shares;
+ rounds.push({ ...fr, round: r, llmPowered: false, quarterSummary: "⚠ LLM unavailable — formula fallback used." });
+ }
+ }
+ onRoundComplete([...rounds]);
+ }
+ return rounds;
+}
+
+/* ═══ UI COMPONENTS ═══ */
+function CornerBrackets({ color = T.green, size = 12, inset = 7 }) {
+ const s = { position: "absolute", width: size, height: size };
+ return (<>
>);
+}
+function Metric({ label, value, sub, color = T.green, compact }) {
+ return (
+
+
{label}
+
{value}
+ {sub &&
{sub}
}
+
);
+}
+function TabBtn({ active, label, onClick }) {
+ return ({label} );
+}
+function ChartTip({ active, payload, label }) {
+ if (!active || !payload?.length) return null;
+ return (
+
Q{label}
+ {payload.filter(p => p.value != null).map((p, i) => (
{p.name}{typeof p.value === "number" ? (Math.abs(p.value) < 1 ? p.value.toFixed(4) : "$"+p.value.toFixed(0)) : p.value}
))}
+
);
+}
+function StimulusCard({ st, active, intensity, onToggle, onInt }) {
+ const pos = st.impact > 0; const ac = pos ? T.green : T.red;
+ return ( onToggle(st.id)} style={{ background: active ? (pos ? T.greenDim : T.redDim) : T.white, border: "1px solid " + (active ? (pos ? T.greenBorder : T.redBorder) : T.border), padding: "7px 9px", cursor: "crosshair", transition: "all 0.15s", userSelect: "none" }}>
+
+
{st.icon}
+
+
{active && ✓ }
+
+ {active && (
e.stopPropagation()}>
+
INT
+
+
{intensity.toFixed(1)}×
+
)}
+
);
+}
+function AgentRow({ agent, expanded, onToggle }) {
+ const s = agent.sentiment || 0;
+ const c = s > 0.1 ? T.green : s < -0.1 ? T.red : T.gold;
+ const act = agent.action || "HOLD";
+ return (
+
+
{agent.icon}
+
+
{(s >= 0 ? "+" : "") + (s * 100).toFixed(0)}%
+
{act}
+
+ {expanded && agent.reasoning && (
+
+
"{agent.reasoning}"
+ {agent.priceTarget &&
PT: ${agent.priceTarget}
}
+
+ )}
+
);
+}
+
+/* ═══ MAIN APP ═══ */
+export default function MiroFishSimulator() {
+ const [stims, setStims] = useState([]);
+ const [catFilter, setCatFilter] = useState("All");
+ const [scenario, setScenario] = useState("base");
+ const [tab, setTab] = useState("simulation");
+ const [mode, setMode] = useState("llm"); // "llm" or "formula"
+ const [live, setLive] = useState(FALLBACK);
+ const [dataStatus, setDataStatus] = useState("loading");
+ const [simData, setSimData] = useState(null); // current scenario data (progressive)
+ const [formulaResults, setFormulaResults] = useState(null); // all 3 scenario formula results
+ const [simStatus, setSimStatus] = useState("idle"); // idle | running | done
+ const [simRound, setSimRound] = useState(0);
+ const [expandedAgent, setExpandedAgent] = useState(null);
+ const abortRef = useRef(false);
+
+ useEffect(() => {
+ let c = false;
+ (async () => { setDataStatus("loading"); const r = await fetchLiveBMNR(); if (c) return; if (r) { setLive(r); setDataStatus("live"); } else setDataStatus("fallback"); })();
+ return () => { c = true; };
+ }, []);
+
+ const cats = useMemo(() => ["All", ...new Set(STIMULI.map(s => s.cat))], []);
+ const filtered = useMemo(() => catFilter === "All" ? STIMULI : STIMULI.filter(s => s.cat === catFilter), [catFilter]);
+ const toggle = useCallback(id => setStims(p => p.find(s => s.id === id) ? p.filter(s => s.id !== id) : [...p, { id, intensity: 1.0 }]), []);
+ const setInt = useCallback((id, v) => setStims(p => p.map(s => s.id === id ? { ...s, intensity: v } : s)), []);
+
+ const runSim = useCallback(async () => {
+ abortRef.current = false;
+ setSimStatus("running"); setSimRound(0); setSimData(null);
+
+ // Always run formula for comparison
+ const fResults = {
+ bear: runFormulaSimulation(stims, "bear", live),
+ base: runFormulaSimulation(stims, "base", live),
+ bull: runFormulaSimulation(stims, "bull", live),
+ };
+ setFormulaResults(fResults);
+
+ if (mode === "llm") {
+ await runFullLLMSimulation(stims, scenario, live, (progressiveData) => {
+ if (abortRef.current) return;
+ setSimData(progressiveData);
+ setSimRound(progressiveData.length - 1);
+ });
+ } else {
+ setSimData(fResults[scenario]);
+ }
+ if (!abortRef.current) setSimStatus("done");
+ }, [stims, scenario, live, mode]);
+
+ const data = simData;
+ const final = data && data.length > 1 ? data[data.length - 1] : null;
+ const initial = data && data.length > 0 ? data[0] : null;
+ const sc = SCENARIO_META[scenario];
+
+ const chartData = data ? data.map(d => ({ round: d.round, stockPrice: d.stockPrice, mNAV: d.mNAV, ethPerShare: d.ethPerShare, sentiment: d.aggSentiment, ethPrice: d.ethPrice })) : [];
+ const gridBg = "linear-gradient(rgba(0,0,0,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(0,0,0,0.02) 1px, transparent 1px)";
+
+ return (
+
+
+
+ {/* NAV */}
+
+
+
+
BMNR
+
MIROFISH SIMULATOR
+
+
+
+ {mode === "llm" ? "▸ LLM-DRIVEN" : "▸ FORMULA"}
+
+
+
+ {dataStatus === "live" ? `LIVE ${live.fetchedAt||""}` : dataStatus === "loading" ? "FETCHING" : "CACHED"}
+
+
+
+
+ {/* TICKER */}
+
+
+ {[...Array(3)].map((_, rep) => (
+ {[`${AGENTS.length} AGENTS`, `${STIMULI.length} STIMULI`, `mNAV ${live.mNAV.toFixed(2)}x`, `${fN(live.ethBalance)} ETH`, `$${live.price} BMNR`, mode === "llm" ? "LLM SWARM REASONING" : "FORMULA ENGINE"].map((t, i) => (
+ ▸ {t}
+ ))}
+
))}
+
+
+
+
+ {/* LEFT */}
+
+ {/* Mode toggle */}
+
+ {[["llm", "LLM AGENTS", T.purple], ["formula", "FORMULA", T.blue]].map(([m, l, c]) => (
+ setMode(m)} style={{ flex: 1, padding: "7px 4px", fontFamily: T.mono, fontSize: 7.5, letterSpacing: "1.5px", cursor: "crosshair", background: mode === m ? c + "12" : T.white, border: "1px solid " + (mode === m ? c : T.border), color: mode === m ? c : T.textDim, fontWeight: mode === m ? 700 : 400 }}>{l}
+ ))}
+
+
+ {mode === "llm" &&
Each quarter, all 21 agents reason via Claude about the market state. Predictions emerge from collective intelligence.
}
+
+ {/* Scenario */}
+
+
+ {(["bear", "base", "bull"]).map(k => {
+ const m = SCENARIO_META[k];
+ return (
setScenario(k)} style={{ flex: 1, padding: "7px 5px", cursor: "crosshair", textAlign: "left", background: scenario === k ? m.color + "10" : T.white, border: "1px solid " + (scenario === k ? m.color : T.border) }}>
+ {m.label}
+ {m.desc.split(",")[0]}
+ );
+ })}
+
+
+ {/* Stimuli */}
+
+
02
+
STIMULI
+ {stims.length > 0 &&
{stims.length} }
+
+
+
+ {cats.map(c => ( setCatFilter(c)} style={{ fontFamily: T.mono, fontSize: 7, letterSpacing: "1px", padding: "2px 7px", cursor: "crosshair", background: catFilter === c ? T.blueDim : "transparent", border: "1px solid " + (catFilter === c ? T.blueBorder : T.border), color: catFilter === c ? T.blue : T.textLight }}>{c.toUpperCase()} ))}
+
+
+ {filtered.map(s => ( x.id === s.id)} intensity={stims.find(x => x.id === s.id)?.intensity || 1} onToggle={toggle} onInt={setInt} />))}
+
+
+
+ {simStatus === "running" ? `▸ SIMULATING Q${simRound}/${SIM_ROUNDS}...` : "▸ RUN SIMULATION"}
+
+ {simStatus === "running" && (
)}
+
+
+ {/* RIGHT */}
+
+ {/* Metrics */}
+ {final && initial ? (
+
+ = initial.stockPrice ? T.green : T.red} />
+ 1 ? T.green : T.red} />
+ = initial.ethPerShare ? T.green : T.red} />
+
+ = 0 ? "+" : "") + (final.aggSentiment * 100).toFixed(0)} sub={`${final.bullish}B / ${final.bearish}Be`} color={final.aggSentiment >= 0 ? T.green : T.red} />
+ {final.llmPowered && }
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+ {["simulation", "agents", "scenarios", "flywheel"].map(t => ( setTab(t)} />))}
+
+
+ {/* SIMULATION TAB */}
+ {tab === "simulation" && (<>
+ {!data || data.length < 2 ? (
+
+ {simStatus === "running" ? (
+ <>
AGENTS REASONING Q{simRound}...
Each agent is analyzing the market via Claude
>
+ ) : (
+ <>
NO SIMULATION YET
Select a scenario, toggle stimuli, and run
>
+ )}
+
+ ) : (<>
+
+
▸ STOCK PRICE · {sc.label} · {data.length - 1} QUARTERS {final?.llmPowered ? "· LLM" : "· FORMULA"}
+
+
+
+
+ "Q" + v} />
+ "$" + v.toFixed(0)} />
+ } />
+
+
+
+
+
+
+ {/* Quarter narrative feed (LLM mode) */}
+ {data.some(d => d.quarterSummary && d.llmPowered) && (
+
+
▸ QUARTER-BY-QUARTER NARRATIVE
+ {data.filter(d => d.round > 0 && d.quarterSummary).map(d => (
+
+
Q{d.round}
+
+ {d.quarterSummary}
+ {d.llmPowered === false && [formula fallback] }
+
+
+ ))}
+
+ )}
+
+ {/* Verdict */}
+ {final && initial && (
+
= initial.stockPrice ? T.greenDim : T.redDim, border: "1px solid " + (final.stockPrice >= initial.stockPrice ? T.greenBorder : T.redBorder) }}>
+
= initial.stockPrice ? T.green : T.red} size={10} inset={5} />
+ = initial.stockPrice ? T.green : T.red, marginBottom: 4 }}>▸ {sc.label} VERDICT {final.llmPowered ? "· LLM-DERIVED" : "· FORMULA"}
+
+ Over {SIM_ROUNDS} quarters, {AGENTS.length} agents drove BMNR from {fmt(initial.stockPrice)} to {fmt(final.stockPrice)} ({pct(safe(final.stockPrice - initial.stockPrice, initial.stockPrice))}).
+ ETH/share: {initial.ethPerShare.toFixed(6)} → {final.ethPerShare.toFixed(6)}. mNAV: {initial.mNAV.toFixed(2)}x → {final.mNAV.toFixed(2)}x.
+ Consensus: {final.bullish} bullish / {final.bearish} bearish.{stims.length > 0 ? ` ${stims.length} stimuli shaped agent reasoning.` : ""}
+
+
+ )}
+ >)}
+ >)}
+
+ {/* AGENTS TAB */}
+ {tab === "agents" && (<>
+
+
+ {(final ? final.agentSnapshot : AGENTS.map(a => ({ ...a, sentiment: a.bias, reasoning: a.persona.split(".")[0] + ".", action: "HOLD" }))).map(a => (
+
setExpandedAgent(expandedAgent === a.id ? null : a.id)} />
+ ))}
+
+
+
▸ {mode === "llm" ? "LLM AGENT REASONING" : "FORMULA AGENT MODEL"}
+
+ {mode === "llm" ? "Each agent's persona is sent to Claude with the full market context. The LLM reasons in-character about what that agent would think, feel, and do — producing emergent collective predictions rather than programmed outcomes. Click any agent row to see their reasoning." : "Agents update sentiment via network propagation formulas. Faster but less realistic — outcomes are pre-determined by equations, not emergent reasoning."}
+
+
+ >)}
+
+ {/* SCENARIOS TAB */}
+ {tab === "scenarios" && formulaResults && (<>
+
+
▸ STOCK PRICE — ALL SCENARIOS (FORMULA)
+
+ ({ round: d.round, bull: formulaResults.bull[i]?.stockPrice, base: formulaResults.base[i]?.stockPrice, bear: formulaResults.bear[i]?.stockPrice }))}>
+
+ "Q" + v} />
+ "$" + v.toFixed(0)} />
+ } />
+
+
+
+
+
+
+
+ {(["bear", "base", "bull"]).map(k => { const m = SCENARIO_META[k]; const f = formulaResults[k]; const fl = f[f.length-1]; const fi = f[0]; return (
+
+
+
{m.label}
+
{m.desc}
+ {[{ l: "FINAL", v: fmt(fl.stockPrice), c: fl.stockPrice >= fi.stockPrice ? T.green : T.red },
+ { l: "RETURN", v: pct(safe(fl.stockPrice - fi.stockPrice, fi.stockPrice)), c: fl.stockPrice >= fi.stockPrice ? T.green : T.red },
+ { l: "mNAV", v: fl.mNAV.toFixed(2) + "x", c: fl.mNAV > 1 ? T.green : T.red },
+ ].map(({ l, v, c }) => (
))}
+
); })}
+
+ >)}
+ {tab === "scenarios" && !formulaResults &&
Run a simulation to compare scenarios
}
+
+ {/* FLYWHEEL TAB */}
+ {tab === "flywheel" && data && data.length > 1 && (<>
+
+
▸ ETH/SHARE ACCRETION
+
+
+ = initial.ethPerShare ? T.green : T.red} stopOpacity={0.15} />= initial.ethPerShare ? T.green : T.red} stopOpacity={0} />
+
+ "Q" + v} />
+ v.toFixed(5)} />
+ } />
+ = initial.ethPerShare ? T.green : T.red} strokeWidth={2} fill="url(#eg)" dot={{ r: 2 }} name="ETH/Shr" />
+
+
+
+
+
+
Q
ISSUED
ETH IN
ETH/SHR
mNAV
PRICE
ACCR?
+
+ {data.slice(1).map(d => (
+
{d.round}
+
{d.sharesIssued > 0 ? fN(d.sharesIssued) : "—"}
+
{d.ethBought > 0 ? fN(d.ethBought) : "—"}
+
{d.ethPerShare.toFixed(6)}
+
{d.mNAV.toFixed(2)}x
+
{fmt(d.stockPrice)}
+
{d.isAccretive ? "YES" : "NO"}
+
))}
+
+ >)}
+ {tab === "flywheel" && (!data || data.length < 2) &&
Run a simulation to see flywheel data
}
+
+
+
+
+ WEDGE × MIROFISH × BMNR
+ EDUCATIONAL ONLY · NOT FINANCIAL ADVICE · {mode === "llm" ? "LLM AGENT-BASED" : "FORMULA"} MODEL
+
+
+ );
+}
\ No newline at end of file
diff --git a/simulator/src/config/index.js b/simulator/src/config/index.js
new file mode 100644
index 00000000..3d48b169
--- /dev/null
+++ b/simulator/src/config/index.js
@@ -0,0 +1,41 @@
+/**
+ * Simulation Configuration
+ *
+ * SIM_ROUNDS: Number of quarters to simulate (8 = 2 years)
+ * SCENARIOS: Bear/Base/Bull case metadata
+ * FALLBACK_DATA: Cached BMNR market data (updated when live fetch fails)
+ */
+
+export const SIM_ROUNDS = 8;
+
+export const SCENARIOS = {
+ bear: { label: "BEAR", color: "#c84b31", desc: "ETH collapses, dilution spiral, premium vanishes" },
+ base: { label: "BASE", color: "#005f99", desc: "ETH $2-3K, MAVAN launches, moderate growth" },
+ bull: { label: "BULL", color: "#007a3d", desc: "ETH breaks ATH, Alchemy 5% hit, staking revenue" },
+};
+
+// Last-known-good BMNR data. Updated by live fetch on load.
+// Source: bitminetracker.io — update these when committing.
+export const FALLBACK_DATA = {
+ price: 23.37,
+ ethPrice: 2314.60,
+ ethBalance: 4595563,
+ shares: 530621703,
+ nav: 22.57,
+ mNAV: 1.04,
+ staked: 3040483,
+ avgCost: 3753.88,
+ cash: 1.2e9,
+ beast: 0.2e9,
+ btcVal: 196 * 85000,
+ analystLo: 30,
+ analystHi: 39,
+ w52Lo: 3.20,
+ w52Hi: 161,
+ fetchedAt: null,
+ isLive: false,
+};
+
+// Default LLM provider config
+export const DEFAULT_PROVIDER = "anthropic";
+export const DEFAULT_MODEL = "claude-sonnet-4-20250514";
diff --git a/simulator/src/data/agents.js b/simulator/src/data/agents.js
new file mode 100644
index 00000000..cb410ab1
--- /dev/null
+++ b/simulator/src/data/agents.js
@@ -0,0 +1,74 @@
+/**
+ * BMNR Agent Personas
+ *
+ * Each agent represents a real market archetype in the BMNR ecosystem.
+ * The `persona` field is a natural-language prompt sent to the LLM —
+ * this is what makes MiroFish different from formula-based models.
+ *
+ * To add a new agent:
+ * 1. Add an entry to this array with a unique `id`
+ * 2. Write a detailed `persona` — the richer, the better reasoning
+ * 3. Set `bias` (-1 to 1) for the formula fallback engine
+ * 4. Set `influence` (0-1) for how much they sway others
+ * 5. Set `susceptibility` (0-1) for how much they follow the herd
+ * 6. Set `memory` (0-1) for how much their past positions carry forward
+ */
+
+export const AGENTS = [
+ // ── Institutional ──
+ { id: "ark", name: "ARK / Cathie Wood", type: "Institutional", icon: "🏛️", bias: 0.5, influence: 0.85, susceptibility: 0.15, memory: 0.7,
+ persona: "You are Cathie Wood's ARK Invest. You are a conviction buyer of disruptive technology. You see BitMine as a leveraged play on Ethereum's transformative potential. You hold millions of BMNR shares and buy dips aggressively. You believe ETH will reach $10K+ and BMNR is massively undervalued." },
+ { id: "fidelity", name: "Fidelity Digital", type: "Institutional", icon: "🏦", bias: 0.2, influence: 0.8, susceptibility: 0.1, memory: 0.8,
+ persona: "You are a Fidelity institutional allocator. You evaluate BMNR on fundamentals: NAV discount/premium, cash flow from staking, dilution risk. You are cautiously optimistic but need to see MAVAN revenue materialize before increasing position." },
+ { id: "mozayyx", name: "MOZAYYX Fund", type: "Institutional", icon: "💎", bias: 0.4, influence: 0.7, susceptibility: 0.2, memory: 0.6,
+ persona: "You are MOZAYYX, a crypto-focused fund with deep conviction in BitMine's Alchemy of 5% strategy. You see the ETH accumulation as a generational opportunity. You rarely sell and add on weakness." },
+ { id: "short_seller", name: "Short Seller Report", type: "Institutional", icon: "🐻", bias: -0.6, influence: 0.65, susceptibility: 0.2, memory: 0.5,
+ persona: "You are an activist short seller. You believe BitMine is a promotion: negative earnings, massive dilution, stock price entirely dependent on ETH price. The 100x share authorization is a red flag. mNAV premium is unjustified. You publish bearish reports." },
+ { id: "pension", name: "Pension Allocator", type: "Institutional", icon: "📊", bias: -0.1, influence: 0.6, susceptibility: 0.25, memory: 0.9,
+ persona: "You are a conservative pension fund. You are skeptical of crypto treasury companies. You need to see stable cash flows, not speculative ETH price appreciation. Dilution concerns dominate your analysis." },
+
+ // ── Insider / Analyst ──
+ { id: "tom_lee", name: "Tom Lee (CEO)", type: "Insider", icon: "👔", bias: 0.7, influence: 0.9, susceptibility: 0.05, memory: 0.3,
+ persona: "You are Tom Lee, CEO of BitMine. You are the architect of the Alchemy of 5% strategy. You believe ETH is severely undervalued and BMNR will be a $100+ stock. You use ATM offerings strategically to accumulate ETH. You dismiss short-seller criticism as lacking vision." },
+ { id: "analyst", name: "B. Riley Analyst", type: "Analyst", icon: "📝", bias: 0.3, influence: 0.7, susceptibility: 0.15, memory: 0.7,
+ persona: "You are a sell-side equity analyst covering BMNR with a price target of $30-39. You focus on mNAV, ETH/share accretion, MAVAN staking revenue potential, and dilution math. You are moderately bullish but flag execution risk." },
+ { id: "ct_analyst", name: "Crypto Twitter Analyst", type: "Analyst", icon: "🧵", bias: 0.2, influence: 0.55, susceptibility: 0.4, memory: 0.4,
+ persona: "You are a crypto-native analyst on X/Twitter. You view BMNR as the best ETH proxy in equities. You track on-chain ETH flows, staking yields, and mNAV daily. You're bullish but aware of the dilution treadmill." },
+ { id: "bear_writer", name: "Seeking Alpha Bear", type: "Analyst", icon: "✍️", bias: -0.5, influence: 0.45, susceptibility: 0.3, memory: 0.6,
+ persona: "You write bearish analysis on Seeking Alpha. You believe BMNR's premium to NAV is irrational, the share dilution is destroying value, and the company has no real revenue. You compare it unfavorably to simply buying ETH directly." },
+
+ // ── Media ──
+ { id: "cnbc", name: "CNBC / Cramer", type: "Media", icon: "📺", bias: 0.0, influence: 0.6, susceptibility: 0.5, memory: 0.2,
+ persona: "You are a financial media personality. You react to price action and headlines. You swing between excitement when BMNR rallies and caution when it drops. You amplify whatever the current narrative is." },
+
+ // ── Retail ──
+ { id: "reddit_bull", name: "r/BMNR Bull", type: "Retail", icon: "🦍", bias: 0.4, influence: 0.3, susceptibility: 0.6, memory: 0.3,
+ persona: "You are a passionate BMNR retail investor on Reddit. You believe in the thesis long-term and buy every dip. You dismiss bears as shorts who 'don't get it'. You post rocket emojis and hold through drawdowns." },
+ { id: "reddit_skeptic", name: "r/BMNR Skeptic", type: "Retail", icon: "🤔", bias: -0.2, influence: 0.25, susceptibility: 0.5, memory: 0.5,
+ persona: "You are a skeptical retail investor. You hold some BMNR but worry about dilution and the gap between stock price and NAV. You ask tough questions and demand clarity on the ATM program." },
+ { id: "wsb", name: "WSB Degen", type: "Retail", icon: "🎰", bias: 0.1, influence: 0.35, susceptibility: 0.8, memory: 0.1,
+ persona: "You are a WallStreetBets trader. You trade BMNR options for volatility. You buy calls before catalysts and puts after euphoria. You have no long-term thesis, only momentum. You follow whatever is trending." },
+ { id: "eth_maxi", name: "ETH Maximalist", type: "Retail", icon: "⟠", bias: 0.0, influence: 0.4, susceptibility: 0.3, memory: 0.4,
+ persona: "You are an Ethereum maximalist. You think owning BMNR is an inefficient way to get ETH exposure with added dilution risk. However, you acknowledge the leveraged upside if ETH moons. You prefer holding ETH directly." },
+ { id: "value", name: "Value Investor", type: "Retail", icon: "📐", bias: -0.3, influence: 0.35, susceptibility: 0.2, memory: 0.8,
+ persona: "You are a Graham-style value investor. You only buy BMNR below NAV (mNAV < 1.0). You think the current premium is speculative. You would be a buyer at $15-18 (discount to NAV) but not at current levels." },
+ { id: "swing", name: "Swing Trader", type: "Retail", icon: "📉", bias: 0.0, influence: 0.2, susceptibility: 0.7, memory: 0.15,
+ persona: "You are a pure technical trader. You only care about chart patterns, volume, and support/resistance levels. Fundamentals are irrelevant to you. You trade the falling wedge pattern and key levels around $20-22." },
+
+ // ── Quant / Algo ──
+ { id: "mm", name: "Market Maker", type: "Quant", icon: "🤖", bias: 0.0, influence: 0.5, susceptibility: 0.0, memory: 0.0,
+ persona: "You are an automated market maker. You provide liquidity and profit from the bid-ask spread. You are always delta neutral. You observe order flow imbalances to gauge short-term direction." },
+ { id: "momentum", name: "Momentum Algo", type: "Quant", icon: "⚡", bias: 0.0, influence: 0.4, susceptibility: 0.0, memory: 0.95,
+ persona: "You are a trend-following algorithm. You buy when price is above its moving averages with increasing volume. You sell when momentum fades. You have no opinion on fundamentals, only price action." },
+ { id: "arb", name: "mNAV Arb Bot", type: "Quant", icon: "🔄", bias: 0.0, influence: 0.45, susceptibility: 0.0, memory: 0.0,
+ persona: "You are an arbitrage algorithm that trades the mNAV premium/discount. When mNAV > 1.3, you short BMNR and buy ETH. When mNAV < 0.8, you buy BMNR and short ETH. You push mNAV toward fair value." },
+
+ // ── Macro ──
+ { id: "macro_bull", name: "Macro Bull", type: "Macro", icon: "🌊", bias: 0.2, influence: 0.5, susceptibility: 0.3, memory: 0.6,
+ persona: "You are a macro strategist who believes we're entering a liquidity-driven bull market. Fed rate cuts, weakening dollar, and crypto adoption create tailwinds for BMNR. Risk-on environments favor leveraged crypto plays." },
+ { id: "macro_bear", name: "Macro Bear", type: "Macro", icon: "🏔️", bias: -0.3, influence: 0.5, susceptibility: 0.3, memory: 0.6,
+ persona: "You are a macro strategist warning about recession risk, sticky inflation, and higher-for-longer rates. Risk assets including crypto are vulnerable. BMNR's leverage to ETH magnifies downside in a risk-off environment." },
+];
+
+export const AGENT_TYPES = [...new Set(AGENTS.map(a => a.type))];
+export const CORE_AGENT_IDS = ["tom_lee", "ark", "short_seller", "analyst", "reddit_bull", "arb", "macro_bull", "macro_bear"];
diff --git a/simulator/src/data/stimuli.js b/simulator/src/data/stimuli.js
new file mode 100644
index 00000000..23913084
--- /dev/null
+++ b/simulator/src/data/stimuli.js
@@ -0,0 +1,53 @@
+/**
+ * Market Stimuli Catalog
+ *
+ * Each stimulus represents a market event that agents react to.
+ * `impact` is the base directional strength (-1 to 1).
+ *
+ * To add a new stimulus:
+ * 1. Add an entry with a unique `id`
+ * 2. Set `cat` to an existing category or create a new one
+ * 3. Set `impact` — positive = bullish, negative = bearish
+ * 4. Write a clear `desc` for the UI
+ */
+
+export const STIMULI = [
+ // ── ETH Ecosystem ──
+ { id: "eth_5k", name: "ETH Breaks $5,000", cat: "ETH", icon: "🚀", impact: 0.7, desc: "Institutional adoption wave" },
+ { id: "eth_crash", name: "ETH Crashes < $800", cat: "ETH", icon: "💀", impact: -0.8, desc: "Crypto winter 3.0" },
+ { id: "eth_etf_in", name: "ETH ETF Mega-Inflows", cat: "ETH", icon: "📈", impact: 0.5, desc: "$10B+ spot ETF flows" },
+ { id: "eth_etf_out", name: "ETH ETF Redemptions", cat: "ETH", icon: "📤", impact: -0.45, desc: "Institutions dump positions" },
+ { id: "eth_upgrade", name: "Pectra Upgrade", cat: "ETH", icon: "⬆️", impact: 0.3, desc: "Protocol upgrade succeeds" },
+
+ // ── BitMine Business ──
+ { id: "mavan", name: "MAVAN Staking Live", cat: "BitMine", icon: "⚡", impact: 0.6, desc: "$330M+ annual staking rev" },
+ { id: "mavan_delay", name: "MAVAN Delayed", cat: "BitMine", icon: "⏳", impact: -0.35, desc: "Technical issues" },
+ { id: "alchemy5", name: "Alchemy 5% Hit", cat: "BitMine", icon: "🧪", impact: 0.65, desc: "5% of all ETH supply" },
+ { id: "beast_ipo", name: "Beast Industries IPO", cat: "BitMine", icon: "🎬", impact: 0.4, desc: "$200M stake monetized" },
+ { id: "scandal", name: "Executive Scandal", cat: "BitMine", icon: "⚠️", impact: -0.6, desc: "Accounting concerns" },
+ { id: "sp500", name: "Index Inclusion", cat: "BitMine", icon: "🏛️", impact: 0.45, desc: "S&P 500 / Russell add" },
+
+ // ── Corporate ──
+ { id: "dilution", name: "100x Auth Used", cat: "Corporate", icon: "💧", impact: -0.55, desc: "Massive dilution" },
+ { id: "buyback", name: "Buyback $500M", cat: "Corporate", icon: "🔄", impact: 0.35, desc: "Aggressive buyback" },
+ { id: "convert", name: "Convertible $2B", cat: "Corporate", icon: "📜", impact: -0.2, desc: "Debt for ETH buys" },
+
+ // ── Macro ──
+ { id: "fed_cut", name: "Fed Cuts 150bps", cat: "Macro", icon: "📉", impact: 0.4, desc: "Aggressive easing" },
+ { id: "recession", name: "US Recession", cat: "Macro", icon: "🏚️", impact: -0.5, desc: "Risk-off everywhere" },
+ { id: "pro_crypto", name: "Pro-Crypto Law", cat: "Macro", icon: "⚖️", impact: 0.4, desc: "Regulatory clarity" },
+ { id: "sec", name: "SEC Crackdown", cat: "Macro", icon: "🚫", impact: -0.6, desc: "Investment co. risk" },
+ { id: "btc_150k", name: "BTC $150K", cat: "Macro", icon: "₿", impact: 0.45, desc: "BTC supercycle" },
+
+ // ── Technical ──
+ { id: "squeeze", name: "Short Squeeze", cat: "Technical", icon: "🔥", impact: 0.6, desc: "30%+ SI unwind" },
+ { id: "ark_exit", name: "ARK Sells All", cat: "Technical", icon: "🚪", impact: -0.5, desc: "Cathie exits" },
+ { id: "rival", name: "Rival Treasury", cat: "Technical", icon: "🏁", impact: -0.25, desc: "Major competitor" },
+
+ // ── Social ──
+ { id: "viral", name: "Viral Social Pump", cat: "Social", icon: "📱", impact: 0.3, desc: "BMNR trends on X" },
+ { id: "fud", name: "Coordinated FUD", cat: "Social", icon: "🗞️", impact: -0.35, desc: "Short-seller report" },
+ { id: "openai", name: "OpenAI/Eightco Win", cat: "Social", icon: "🤖", impact: 0.35, desc: "$80M bet pays off" },
+];
+
+export const STIMULUS_CATEGORIES = [...new Set(STIMULI.map(s => s.cat))];
diff --git a/simulator/src/engine/prompts.js b/simulator/src/engine/prompts.js
new file mode 100644
index 00000000..3067c250
--- /dev/null
+++ b/simulator/src/engine/prompts.js
@@ -0,0 +1,71 @@
+/**
+ * MiroFish Prompt Builder
+ *
+ * Constructs the prompt sent to the LLM each simulation round.
+ * This is the most important file for simulation accuracy —
+ * the quality of the prompt directly determines the quality
+ * of agent reasoning.
+ *
+ * Fork this to customize for other stocks / assets.
+ */
+
+import { AGENTS, CORE_AGENT_IDS } from "../data/agents.js";
+import { STIMULI } from "../data/stimuli.js";
+
+function fN(n) {
+ if (!isFinite(n)) return "—";
+ if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(2) + "M";
+ if (Math.abs(n) >= 1e3) return (n / 1e3).toFixed(1) + "K";
+ return n.toFixed(0);
+}
+
+/**
+ * Select which agents to poll this round.
+ * Core agents are always included; others rotate.
+ */
+export function selectRoundAgents(roundNum) {
+ const rotatingIds = AGENTS.filter(a => !CORE_AGENT_IDS.includes(a.id)).map(a => a.id);
+ const start = ((roundNum - 1) * 3) % rotatingIds.length;
+ const extras = [0, 1, 2].map(i => rotatingIds[(start + i) % rotatingIds.length]);
+ const ids = [...new Set([...CORE_AGENT_IDS, ...extras])];
+ return AGENTS.filter(a => ids.includes(a.id));
+}
+
+/**
+ * Build the user message for a simulation round.
+ */
+export function buildRoundPrompt(roundNum, totalRounds, marketState, activeStimuli, prevSummary, scenarioMode) {
+ const stimDesc = activeStimuli.map(s => {
+ const st = STIMULI.find(x => x.id === s.id);
+ return st ? `${st.name} (${(s.intensity || 1).toFixed(1)}x) — ${st.desc}` : "";
+ }).filter(Boolean).join("; ");
+
+ const roundAgents = selectRoundAgents(roundNum);
+ const agentList = roundAgents.map(a =>
+ `- ${a.name} [id:${a.id}] (${a.type}): ${a.persona.split(".").slice(0, 2).join(".")}.`
+ ).join("\n");
+
+ const scenarioLean = {
+ bear: "bearish — things go wrong, sentiment deteriorates",
+ base: "moderate — mixed signals, gradual progress",
+ bull: "bullish — catalysts hit, momentum builds",
+ }[scenarioMode];
+
+ return `You are a financial market simulation engine. You MUST respond with ONLY a raw JSON object. No markdown fences, no backticks, no explanation before or after the JSON. Just the raw JSON starting with { and ending with }.
+
+SCENARIO: ${scenarioMode.toUpperCase()} CASE | QUARTER: Q${roundNum} of ${totalRounds}
+
+MARKET STATE:
+BMNR: $${marketState.stockPrice.toFixed(2)} | ETH: $${marketState.ethPrice.toFixed(0)} | mNAV: ${marketState.mNAV.toFixed(2)}x | NAV/Shr: $${marketState.navPerShare.toFixed(2)} | ETH/Shr: ${marketState.ethPerShare.toFixed(6)} | Holdings: ${fN(marketState.ethHoldings)} ETH (${(marketState.ethHoldings / 120e6 * 100).toFixed(1)}% supply) | Breakeven: ${marketState.breakevenMNav.toFixed(2)}x | Flywheel: ${marketState.mNAV > marketState.breakevenMNav ? "ACCRETIVE" : "DILUTIVE"}
+
+${stimDesc ? `EVENTS: ${stimDesc}` : "NO EVENTS"}
+${prevSummary ? `PREV QUARTER: ${prevSummary}` : "First quarter."}
+
+AGENTS:
+${agentList}
+
+For the ${scenarioMode.toUpperCase()} case, determine each agent's reaction. Lean ${scenarioLean}. Think about herding, contrarianism, reflexivity.
+
+Respond with ONLY this JSON structure (no other text):
+{"agents":[{"id":"agent_id","sentiment":-1.0 to 1.0,"action":"BUY or SELL or HOLD","reasoning":"1 sentence","priceTarget":30}],"ethPriceChange":0.05,"mNavChange":0.02,"quarterSummary":"2 sentences"}`;
+}
diff --git a/simulator/src/main.jsx b/simulator/src/main.jsx
new file mode 100644
index 00000000..ce286a9b
--- /dev/null
+++ b/simulator/src/main.jsx
@@ -0,0 +1,5 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App.jsx";
+
+createRoot(document.getElementById("root")).render( );
diff --git a/simulator/src/providers/index.js b/simulator/src/providers/index.js
new file mode 100644
index 00000000..0a128ab3
--- /dev/null
+++ b/simulator/src/providers/index.js
@@ -0,0 +1,274 @@
+/**
+ * LLM Provider Abstraction Layer
+ *
+ * Add your own provider by implementing the LLMProvider interface:
+ * - id: unique string identifier
+ * - name: display name
+ * - call(messages, options): async function returning { text: string }
+ *
+ * Supported out of the box:
+ * - Anthropic (Claude) — via claude.ai artifact API or direct API
+ * - OpenAI (GPT-4, etc.) — via direct API
+ * - Google (Gemini) — via direct API
+ * - OpenRouter — meta-provider supporting 100+ models
+ * - Ollama — local models, no API key needed
+ * - Custom — bring your own endpoint
+ */
+
+// ─── JSON PARSER (shared across all providers) ───
+export function parseJSON(text) {
+ if (!text || text.length < 10) return null;
+
+ // Strip markdown fences
+ let cleaned = text.replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
+
+ // Try direct parse
+ try { return JSON.parse(cleaned); } catch {}
+
+ // Fallback: find outermost { } containing "agents"
+ const idx = cleaned.indexOf('"agents"');
+ if (idx === -1) return null;
+ let start = cleaned.lastIndexOf("{", idx);
+ if (start === -1) return null;
+ let depth = 0, end = -1;
+ for (let i = start; i < cleaned.length; i++) {
+ if (cleaned[i] === "{") depth++;
+ else if (cleaned[i] === "}") { depth--; if (depth === 0) { end = i; break; } }
+ }
+ if (end === -1) return null;
+ try { return JSON.parse(cleaned.slice(start, end + 1)); } catch { return null; }
+}
+
+// ─── ANTHROPIC PROVIDER ───
+export const AnthropicProvider = {
+ id: "anthropic",
+ name: "Anthropic (Claude)",
+ models: ["claude-sonnet-4-20250514", "claude-haiku-4-5-20251001"],
+ defaultModel: "claude-sonnet-4-20250514",
+ requiresKey: false, // false when running inside claude.ai artifacts
+
+ async call(messages, { model, maxTokens = 4096, apiKey } = {}) {
+ const headers = { "Content-Type": "application/json" };
+ if (apiKey) {
+ headers["x-api-key"] = apiKey;
+ headers["anthropic-version"] = "2023-06-01";
+ }
+
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
+ method: "POST",
+ headers,
+ body: JSON.stringify({
+ model: model || this.defaultModel,
+ max_tokens: maxTokens,
+ messages,
+ }),
+ });
+
+ if (!res.ok) {
+ const err = await res.text().catch(() => "");
+ throw new Error(`Anthropic ${res.status}: ${err.slice(0, 200)}`);
+ }
+
+ const data = await res.json();
+ const text = (data.content || []).filter(b => b.type === "text").map(b => b.text).join("\n");
+ return { text, raw: data };
+ }
+};
+
+// ─── OPENAI PROVIDER ───
+export const OpenAIProvider = {
+ id: "openai",
+ name: "OpenAI (GPT-4)",
+ models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o3-mini"],
+ defaultModel: "gpt-4o",
+ requiresKey: true,
+
+ async call(messages, { model, maxTokens = 4096, apiKey } = {}) {
+ if (!apiKey) throw new Error("OpenAI requires an API key. Set it in Settings → LLM Provider.");
+
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
+ body: JSON.stringify({
+ model: model || this.defaultModel,
+ max_tokens: maxTokens,
+ messages: messages.map(m => ({ role: m.role, content: m.content })),
+ temperature: 0.7,
+ }),
+ });
+
+ if (!res.ok) {
+ const err = await res.text().catch(() => "");
+ throw new Error(`OpenAI ${res.status}: ${err.slice(0, 200)}`);
+ }
+
+ const data = await res.json();
+ const text = data.choices?.[0]?.message?.content || "";
+ return { text, raw: data };
+ }
+};
+
+// ─── GOOGLE GEMINI PROVIDER ───
+export const GeminiProvider = {
+ id: "gemini",
+ name: "Google (Gemini)",
+ models: ["gemini-2.5-flash", "gemini-2.5-pro"],
+ defaultModel: "gemini-2.5-flash",
+ requiresKey: true,
+
+ async call(messages, { model, maxTokens = 4096, apiKey } = {}) {
+ if (!apiKey) throw new Error("Gemini requires an API key from ai.google.dev.");
+
+ const m = model || this.defaultModel;
+ const contents = messages.map(msg => ({
+ role: msg.role === "assistant" ? "model" : "user",
+ parts: [{ text: msg.content }],
+ }));
+
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${m}:generateContent?key=${apiKey}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ contents,
+ generationConfig: { maxOutputTokens: maxTokens, temperature: 0.7 },
+ }),
+ });
+
+ if (!res.ok) {
+ const err = await res.text().catch(() => "");
+ throw new Error(`Gemini ${res.status}: ${err.slice(0, 200)}`);
+ }
+
+ const data = await res.json();
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
+ return { text, raw: data };
+ }
+};
+
+// ─── OPENROUTER PROVIDER (100+ models) ───
+export const OpenRouterProvider = {
+ id: "openrouter",
+ name: "OpenRouter (Multi-Model)",
+ models: [
+ "anthropic/claude-sonnet-4",
+ "openai/gpt-4o",
+ "google/gemini-2.5-flash",
+ "meta-llama/llama-4-maverick",
+ "deepseek/deepseek-r1",
+ "mistralai/mistral-large-latest",
+ ],
+ defaultModel: "anthropic/claude-sonnet-4",
+ requiresKey: true,
+
+ async call(messages, { model, maxTokens = 4096, apiKey } = {}) {
+ if (!apiKey) throw new Error("OpenRouter requires an API key from openrouter.ai.");
+
+ const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${apiKey}`,
+ "HTTP-Referer": "https://github.com/mirofish-bmnr",
+ "X-Title": "MiroFish BMNR Simulator",
+ },
+ body: JSON.stringify({
+ model: model || this.defaultModel,
+ max_tokens: maxTokens,
+ messages: messages.map(m => ({ role: m.role, content: m.content })),
+ }),
+ });
+
+ if (!res.ok) {
+ const err = await res.text().catch(() => "");
+ throw new Error(`OpenRouter ${res.status}: ${err.slice(0, 200)}`);
+ }
+
+ const data = await res.json();
+ const text = data.choices?.[0]?.message?.content || "";
+ return { text, raw: data };
+ }
+};
+
+// ─── OLLAMA PROVIDER (local, free) ───
+export const OllamaProvider = {
+ id: "ollama",
+ name: "Ollama (Local)",
+ models: ["llama3.1:70b", "llama3.1:8b", "mixtral:8x7b", "qwen2.5:32b", "deepseek-r1:32b", "gemma3:27b"],
+ defaultModel: "llama3.1:70b",
+ requiresKey: false,
+
+ async call(messages, { model, maxTokens = 4096, baseUrl = "http://localhost:11434" } = {}) {
+ const res = await fetch(`${baseUrl}/api/chat`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ model: model || this.defaultModel,
+ messages: messages.map(m => ({ role: m.role, content: m.content })),
+ stream: false,
+ options: { num_predict: maxTokens, temperature: 0.7 },
+ }),
+ });
+
+ if (!res.ok) {
+ const err = await res.text().catch(() => "");
+ throw new Error(`Ollama ${res.status}: ${err.slice(0, 200)}`);
+ }
+
+ const data = await res.json();
+ const text = data.message?.content || "";
+ return { text, raw: data };
+ }
+};
+
+// ─── CUSTOM OPENAI-COMPATIBLE PROVIDER ───
+export const CustomProvider = {
+ id: "custom",
+ name: "Custom (OpenAI-compatible)",
+ models: ["default"],
+ defaultModel: "default",
+ requiresKey: true,
+
+ async call(messages, { model, maxTokens = 4096, apiKey, baseUrl } = {}) {
+ if (!baseUrl) throw new Error("Custom provider requires a base URL. Set it in Settings.");
+
+ const headers = { "Content-Type": "application/json" };
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
+
+ const res = await fetch(`${baseUrl}/v1/chat/completions`, {
+ method: "POST",
+ headers,
+ body: JSON.stringify({
+ model: model || "default",
+ max_tokens: maxTokens,
+ messages: messages.map(m => ({ role: m.role, content: m.content })),
+ }),
+ });
+
+ if (!res.ok) {
+ const err = await res.text().catch(() => "");
+ throw new Error(`Custom ${res.status}: ${err.slice(0, 200)}`);
+ }
+
+ const data = await res.json();
+ const text = data.choices?.[0]?.message?.content || "";
+ return { text, raw: data };
+ }
+};
+
+// ─── PROVIDER REGISTRY ───
+export const PROVIDERS = {
+ anthropic: AnthropicProvider,
+ openai: OpenAIProvider,
+ gemini: GeminiProvider,
+ openrouter: OpenRouterProvider,
+ ollama: OllamaProvider,
+ custom: CustomProvider,
+};
+
+export function getProvider(id) {
+ return PROVIDERS[id] || AnthropicProvider;
+}
+
+export function getAllProviders() {
+ return Object.values(PROVIDERS);
+}
diff --git a/simulator/vite.config.js b/simulator/vite.config.js
new file mode 100644
index 00000000..5327fa03
--- /dev/null
+++ b/simulator/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: { port: 3000 },
+});