HTTP API Reference

June 16, 2026 · View on GitHub

English · 中文

Base URL is http://localhost:5001 in dev. Every endpoint returns JSON unless otherwise noted.

Interactive docs: the running backend serves Swagger UI at /api/docs and the OpenAPI 3.1 spec at /api/openapi.yaml (or /api/openapi.json). Point openapi-generator at the spec to produce a Python / TypeScript / Go SDK in one command.

Localization

Every endpoint resolves an active locale per request. Order of precedence:

  1. ?lang= query parameter (use on share-surface URLs that need to be locale-pinned in their canonical form — /share/<id>, cite.bib, badge.svg, chart.svg)
  2. X-MiroShark-Locale request header (what the bundled SPA sends on every API call)
  3. Accept-Language request header (standard fallback for off-the-shelf HTTP clients)
  4. en (default)

Supported locales: en, zh-CN. Unknown tags fall back to en silently — ?lang=fr on the current build returns the English payload, not a 400.

What gets localized: API error messages ({"success": false, "error": "..."}), preset template name / description metadata, RSS / Atom feed copy, and the report agent's narration. Raw simulation data (posts, trades, agent stances) is never translated.

# SPA-style: header-driven
curl -s -H "X-MiroShark-Locale: zh-CN" "https://your-host/api/templates/list"

# Share-surface-style: query-pinned for stable canonical URLs
curl -s "https://your-host/share/<id>?lang=zh-CN"

Setup & Discovery

MethodPathPurpose
POST/api/simulation/suggest-scenariosScenario auto-suggest (Bull / Bear / Neutral) from a document preview
GET/api/simulation/trendingPull RSS/Atom items for the "What's Trending" panel
POST/api/simulation/askJust Ask — synthesize a seed briefing from a question
POST/api/graph/fetch-urlFetch + extract text from a URL
GET/api/templates/listPreset templates
GET/api/templates/<id>?enrich=trueTemplate + live FeedOracle enrichment

Graph Build (Step 1)

MethodPathPurpose
POST/api/graph/ontology/generateNER + ontology extraction
POST/api/graph/buildBuild Neo4j graph from ontology
GET/api/graph/task/<task_id>Poll async task status
GET/api/graph/data/<graph_id>Paginated graph nodes + edges
GET/api/simulation/entities/<graph_id>Browse entities
GET/api/simulation/entities/<graph_id>/<uuid>Single entity + neighborhood

Simulation Lifecycle

MethodPathPurpose
POST/api/simulation/createCreate simulation from seed + prompt
POST/api/simulation/prepareKick off profile generation (Step 2)
POST/api/simulation/prepare/statusPoll Step 2
POST/api/simulation/startLaunch Wonderwall subprocess (Step 3)
POST/api/simulation/stopTerminate
POST/api/simulation/branch-counterfactualFork with counterfactual injection
POST/api/simulation/forkDuplicate config
POST/api/simulation/<id>/director/injectDirector mode — live event injection
GET/api/simulation/<id>/director/eventsList director events

Live State & Data

MethodPathPurpose
GET/api/simulation/<id>/run-statusCurrent round / totals
GET/api/simulation/<id>/run-status/detailPer-platform progress
GET/api/simulation/<id>/frame/<round>Compact per-round snapshot
GET/api/simulation/<id>/timelineRound-by-round summary
GET/api/simulation/<id>/actionsRaw agent action log
GET/api/simulation/<id>/postsPaginated posts (Twitter + Reddit)
GET/api/simulation/<id>/profilesAgent personas
GET/api/simulation/<id>/profiles/realtimeLive belief updates
GET/api/simulation/<id>/polymarket/marketsMarkets + current prices
GET/api/simulation/<id>/polymarket/market/<mid>/pricesPrice history

Analytics

MethodPathPurpose
GET/api/simulation/<id>/belief-driftStance drift per topic per round
GET/api/simulation/<id>/counterfactualOriginal vs branch comparison
GET/api/simulation/<id>/agent-statsPer-agent engagement + posting
GET/api/simulation/<id>/influenceInfluence leaderboard
GET/api/simulation/<id>/interaction-networkAgent-to-agent graph
GET/api/simulation/<id>/demographicsArchetype distribution
GET/api/simulation/<id>/qualityRun health diagnostics
GET/api/simulation/<id>/peak-roundMachine-readable belief inflection points — the round each stance (bullish / neutral / bearish) peaked ({round, pct}), the most_volatile_round (largest summed round-over-round swing), max_swing_pct, and total_rounds. Pure O(n) derivation from the same ±0.2 stance split trajectory.csv uses; peak ties resolve to the earliest round. Publish-gated; 404 until trajectory data exists. Cached 5 minutes. curl -s "https://your-host/api/simulation/<id>/peak-round"
GET/api/simulation/<id>/volatilityBelief-volatility analytics — the distribution of round-over-round belief swings: mean_delta_pct, std_dev_delta_pct, max_delta_pct, max_delta_round, plus a normalized volatility_index on [0, 100] (min(std_dev × 5, 100)) and a trend label (stable / converging / contested). Same `
GET/api/simulation/<id>/agents/sparklinesPer-agent belief sparklines — for each agent, a {round, position} series the frontend draws as a compact SVG line, plus final_stance / final_position / color. The agent-level layer under chart.svg's aggregate curve. Same _avg_position mean + ±0.2 threshold every surface uses; agent names from reddit_profiles.json. Agents ordered most-bullish-first; has_per_agent_data is false for single-round sims. Publish-gated; 404 until per-agent data exists. Cached 5 minutes. curl -s "https://your-host/api/simulation/<id>/agents/sparklines"
GET/api/simulation/<id>/cost.jsonEstimated run cost — the "$1 to simulate anything" claim as structured JSON. Headline estimated_cost_usd plus totals (LLM calls, tokens in/out/total, LLM seconds), a latency_ms distribution (p50 / p90 / max), wall_clock start/end, and by_model + by_phase breakdowns (each row: calls, tokens, errors, cost). Priced off the same OpenRouter table run_summary.md uses, so the JSON and the on-disk report never disagree. is_estimate is always true and pricing_basis documents the method — calls on untracked models count as $0, so the figure is a lower bound. Publish-gated; 404 until the sim has logged an LLM call. Cached 60 seconds. curl -s "https://your-host/api/simulation/<id>/cost.json"
POST/api/simulation/compareSide-by-side belief comparison
POST/api/simulation/batch-statusMulti-sim status lookup — poll up to 20 simulations in one call. Body {"sim_ids": [...]} (1–20 ids; ids match ^[A-Za-z0-9_\-.]{1,128}$, anything else → 400). Response is {schema_version, count, results} where results preserves input order; each entry carries sim_id, found, status, current_round, total_rounds, direction, confidence_pct, quality_health, completed_at. Private + unknown ids return the identical {found: false, ...nulls} envelope so a caller can't distinguish private from non-existent (publish gate applied per id; the endpoint is unauthenticated). Running / failed / cancelled sims return the bare status + null analytics (only completed sims carry signal fields, matching the per-sim signal.json byte-for-byte). Duplicates in sim_ids emit duplicate entries — the surface trusts the caller's batching. The N-to-1 replacement for polling /api/simulation/<id>/run-status per id; built for AntFleet, Capacitr, and the rest of the ecosystem table running parallel benchmark batches. Cache-Control: no-store

Interaction

MethodPathPurpose
POST/api/simulation/interviewChat with one agent
POST/api/simulation/interview/batchAsk a group in parallel
POST/api/simulation/<id>/agents/<name>/trace-interviewChat with full reasoning trace
GET/api/simulation/<id>/interviews/<name>Past transcripts with an agent

Publish / Embed / Export

MethodPathPurpose
POST/api/simulation/<id>/publishToggle is_public
POST/api/simulation/<id>/share-linkMint a private share-link token (admin-gated; body {expires_in_days?}, default 30, clamped [1, 365]). Returns {token, preview_url, share_url, expires_at_iso, expires_in_days}. The preview_url (/preview/<token>) bypasses the is_public gate for the preview page only — it does not publish the sim, list it on the gallery, or unlock the per-sim REST surfaces. noindex,nofollow + no Open Graph tags on the resolved page, so a leaked URL doesn't auto-unfurl in Discord/Slack/Twitter. Use for stakeholder previews, pre-publication review, and selective handoff to a co-author
GET/api/simulation/<id>/share-linksList active (non-revoked, non-expired) share-link tokens for a sim, newest-first by created_at_epoch. Admin-gated. Each entry carries token, preview_url, share_url, created_at_iso, expires_at_iso, expires_in_days_remaining, expires_in_seconds_remaining
DELETE/api/simulation/<id>/share-link/<token>Revoke one share-link token. Idempotent — returns 204 whether the token existed or had already been revoked. Admin-gated. The associated /preview/<token> URL stops resolving immediately (no proxy caching)
GET/preview/<token>Private-preview landing page resolved from a share-link token. Returns the same SPA redirect as /share/<id> but with <meta name="robots" content="noindex,nofollow">, X-Robots-Tag: noindex,nofollow, Referrer-Policy: no-referrer, and no Open Graph / Twitter / Farcaster Frame / oEmbed tags — so the preview can be shared with one person without being indexed or auto-unfurled. Unknown / revoked / expired tokens all return the same 404 body ("This preview link is no longer valid.") so a probe can't distinguish the cases. Cache-Control: no-store
GET/api/simulation/<id>/embed-summaryEmbed payload (public sims only)
GET`/api/simulation//share-card.png$1200 \times 630 \text{OG} \text{image} (\text{auto}-\text{unfurls})
$GET`/api/simulation/<id>/replay.gifAnimated belief-bar replay
GET/api/simulation/<id>/transcript.mdMarkdown transcript (Notion / Obsidian / Substack)
GET/api/simulation/<id>/transcript.jsonStructured JSON transcript (SDKs / LLM-as-judge)
GET/api/simulation/<id>/trajectory.csvPer-round belief CSV (pandas.read_csv() / Excel / Tableau / R)
GET/api/simulation/<id>/trajectory.jsonlPer-round belief JSONL (DuckDB / pipelines)
GET/api/simulation/<id>/chart.svgPer-round belief chart as a stdlib-rendered SVG — embed as <img> in Notion / Substack / Ghost / GitHub READMEs / LaTeX. Same ±0.2 stance threshold; vector scales to any size with no JavaScript
GET/api/simulation/<id>/badge.svgFlat Shields.io-compatible 20-pixel status badge — MiroShark left half, {direction} {confidence}% right half coloured by stance (#22c55e Bullish / #6b7280 Neutral / #ef4444 Bearish). Embed in any GitHub README, Notion page, Substack post, or personal site as a one-line Markdown image; the live signal updates as the sim runs
GET/api/simulation/<id>/frame-metadataFarcaster Frame v2 metadata — frame_version, image_url (chart SVG, falling back to share card), image_aspect_ratio, share_url, buttons, has_trajectory. The matching fc:frame:* meta tags are emitted by the share page so a Farcaster cast containing the share URL renders as an interactive Frame card
GET/api/simulation/<id>/thread.txtAuto-formatted X / Twitter tweet thread (one tweet per belief inflection point, ≤280 chars each)
GET/api/simulation/<id>/thread.jsonSame tweet thread as thread.txt but as {tweets, total, inflections_recorded, truncated} for programmatic consumers
GET/api/simulation/<id>/signed-result.jsonHMAC-SHA256 signed wrapper around signal.json — the same final-state signal fields plus a signature over the canonical JSON encoding of the inner result block (json.dumps(result, sort_keys=True, separators=(",", ":"), ensure_ascii=True)). Signing key is the deployment's WEBHOOK_SECRET (same secret the outbound webhook delivery service uses; no new config). Gives integrators an offline-verifiability primitive — once a result is stored in a database, settlement ledger, or research archive, the signature lets the recipient prove the stored bytes match what MiroShark emitted, without re-fetching live. Pairs with cite.bib (on-chain DKG UAL) for dual-coverage authenticity. Verification recipe in Python: hmac.new(SECRET.encode(), json.dumps(env["result"], sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode(), hashlib.sha256).hexdigest()env["signature"] with hmac.compare_digest. Deployments without WEBHOOK_SECRET still get 200 with signed=false + the unsigned result. Same publish gate as signal.json (403 private / 404 not-ready). Cached 5 minutes
GET/api/simulation/<id>/polymarket.jsonPolymarket-shaped binary-market prediction — yes_probability / no_probability (sum to 1.0), confidence_tier (four-bucket discrete scale on top of signal.json's continuous confidence_pct), the underlying belief percentages, and a suggested_market_title shaped as "Will …?". Stricter publish gate than signal.json: only completed sims emit a payload (a Polymarket bot acting on a mid-run signal would chase numbers that can still flip). Cached 5 minutes
GET/api/simulation/<id>/clone.jsonClone payload — the first surface that returns the sim's inputs rather than its outputs. clone_payload is wire-compatible with POST /api/simulation/create (same field set, same defaults): a caller with the same project_id re-runs the sim with one curl; an AntFleet benchmark fork swaps polymarket_market_count from 1 to 5 and POSTs. simulation_requirement is echoed alongside for context (it lives at the project level rather than the create body). Cached 1 hour — these inputs don't shift round-to-round
GET/api/simulation/<id>/agents.jsonAgent roster export — the participants surface. Each agent's name, username, bio, persona_preview (truncated to 280 chars), demographics (age / gender / mbti / country / profession / interested_topics), karma, plus final_stance / final_position / rounds_participated derived from the same trajectory.json the sparklines read. Same ±0.2 stance threshold every other surface uses, so an agent tagged bullish here is bullish in the transcript and sparkline. Agents ordered most-bullish-first by final position; profile-only agents (no belief data) appear at the bottom with final_stance=neutral, final_position=null, rounds_participated=0. The companion to agents/sparklines (which shows belief trajectories): this surface answers who was in the debate. Publish-gated; 404 until profile data exists. Cached 1 hour — the roster is structural
GET/api/simulation/<id>/surface-statsPer-share-surface request counters — share card / replay GIF / transcript / trajectory / chart.svg / thread / watch page / Atom / RSS / reproduce.json / lineage / notebook.ipynb, plus a synthetic total
GET/api/simulation/<id>/reproduce.jsonCitation primitive — v1-schema reproducibility config blob carrying scenario, agent count, total rounds, platform toggles, time-config knobs, director events, and fork / counterfactual lineage. Identical exports of a finished sim are bytewise-identical (citation-hash friendly)
GET/api/simulation/<id>/cite.bibDrop-in BibTeX @misc{…} academic citation entry. Imports cleanly into Zotero / Mendeley via "Import from URL"; the note field carries the reproduce.json SHA-256 (verifiable via sha256sum --check); the annote field carries the OriginTrail DKG UAL when the sim has been anchored on-chain. text/plain; charset=utf-8
GET/api/simulation/<id>/notebook.ipynbPre-populated Jupyter notebook — trajectory CSV embedded directly + belief-evolution / final-consensus / quality-summary cells scaffolded. Runs air-gapped (no network call back required). nbformat 4 JSON. Same bytewise-stable property as the reproduce.json blob
GET/api/simulation/<id>/lineageLineage graph slice — parent the sim was forked / branched from + every public child whose parent_simulation_id points back at it. Closes the navigation gap the reproduction config left open
GET/api/simulation/<id>/webhook-logRecent outbound-webhook delivery attempts (last 10 + total count). Admin-token gated
POST/api/simulation/<id>/webhook-retryRe-fire the completion webhook for a finished sim. Admin-token gated
GET/share/<id>Public OG-tag landing page (auto-redirects to SPA). Also emits application/json+oembed + text/xml+oembed discovery <link> tags for published sims
GET/oembed?url=<share-url>&format=<json|xml>oEmbed 1.0 provider — auto-unfurls a pasted /share/<id> link into a type: "rich" card (share-card thumbnail + /embed/<id> iframe) on Notion, Ghost, Substack, WordPress, and other oEmbed consumers. format defaults to json; unsupported format → 501. Foreign-domain / unpublished / missing URL → 404. Honors X-Forwarded-*
GET/watch/<id>Live spectator-watch page — minimal full-viewport broadcast view, polls every 15 s, OG / Twitter-card unfurl
GET/sitemap.xmlAuto-generated sitemap (sitemaps.org 0.9) listing every public sim's /share/<id> + /watch/<id> URLs. 404 when ENABLE_SITEMAP=false. Cached 1 h
GET/robots.txtCrawler directives — Disallow: /api/, Allow: /share/ etc., advertises Sitemap: when enabled. Cached 1 h
GET/api/config/sitemapPublic flag {enabled, sitemap_url} exposed to the SPA so EmbedDialog renders the right indexing hint
GET/api/statsPlatform-level aggregate stats — total_sims (public + completed), consensus_distribution (bullish/neutral/bearish counts + pcts via the same plurality rules signal.json uses), avg_confidence_pct, total_surface_views (sum across every sim's surface-stats.json), unique_projects, newest_sim_id + newest_sim_created_at. ETag derived from total_sims + newest_sim_id so If-None-Match short-circuits to 304. Cached 60 s
GET/api/stats/distribution.jsonPlatform-wide outcome distribution — bucketed breakdowns across four dimensions: by_direction (bullish/neutral/bearish, same plurality + tie-break as signal.json), by_confidence (high ≥70 / medium 40-70 / low <40), by_quality (excellent/good/fair/poor from quality.json.health), by_round_count (short <10 / medium 10-20 / long >20 recorded snapshots). Plus avg_confidence_pct + avg_total_rounds scalars. Same publish gate as /api/stats (public + completed) so total_analyzed is a strict subset of /api/stats.total_sims consumers' mental model. ETag derived from total_analyzed + year-month prefix of newest_completed_at. Empty deployment returns all-zero envelope, never 404s. Cached 300 s
GET/api/stats/badge.svgFlat Shields.io-compatible platform badge — MiroShark left half (#555555), N simulations right half (platform-blue #0ea5e9). Sibling of the per-sim badge.svg — embed in any community README, Substack, or operator portfolio to advertise live platform activity. Always returns 200 (a zero-sim deployment renders `MiroShark
GET/api/surfaces.jsonMachine-readable catalog of every share / platform surface this deployment exposes. Each entry carries key, endpoint (with <simulation_id> placeholder where relevant), method, type (one of analytics / visualization / export / embed / integration / platform / discovery), description (≤120 chars), added_in_pr, and a copy-pasteable example_curl. Optional ?type=<category> narrows the catalog to one category server-side (count reflects the filtered length; unknown value → 400). ETag short-circuits to 304 until a new surface ships (filtered requests carry the category in the ETag). Returns 200 or 304; only a bad ?type= returns 400. Cached 1 h
GET/api/ecosystem.jsonMachine-readable counterpart of ECOSYSTEM.md — every external project, agent, and product publicly identified as built on MiroShark. Each entry carries name, url, description (≤160 chars), category (one of product / tool / integration / agent / benchmark), x_handle (without leading @, nullable), and repo (https://github.com/… URL, nullable). Alphabetised by name to match ECOSYSTEM.md ordering. ETag short-circuits to 304 until a new integrator ships. Always returns 200 or 304. Cached 1 h
GET/api/project/<project_id>/statsPer-project aggregate stats — total_sims (public + completed for this project), published_sims (alias), consensus_distribution (same plurality rules as signal.json), avg_confidence_pct, quality_distribution (excellent / good / fair / poor buckets — new in this surface), total_surface_views, newest_sim_id + newest_sim_created_at. Per-project sibling of /api/stats. project_id must match [A-Za-z0-9_.-]{1,120} (400 otherwise); unknown project_id returns an all-zero envelope, not 404. ETag "project-<total>-<newest_id_prefix>" short-circuits to 304. Cached 60 s
GET/api/status.jsonPlatform health probe — ok: true + queue_depth (running sims) + completed_24h (forward-progress count) + last_completed_at (most recent completion ISO) + total_sims (lifetime cumulative — public + private + in-flight + failed, distinct from /api/stats.total_sims which is gated to public + completed) + surface_count (sourced from /api/surfaces.json catalog) + check_at (ISO-8601 UTC). The answer to "is this MiroShark instance up and completing sims?" — built for external status monitors (Upptime, BetterUptime, Statuspage.io), Aeon heartbeat skills, and integrator pre-flight checks. completed_24h uses updated_at (the completion timestamp) so a sim created weeks ago but completed in the last 24 h still counts. No in-process cache (the surface is meant to be live); Cache-Control: public, max-age=30. Empty deployment returns the all-zero envelope, still ok: true — never 404s
GET/api/activity.jsonLightweight what-just-completed feed — N most recently completed public sims in reverse-chronological order, each carrying sim_id, scenario_title (truncated to 100 chars), direction + confidence_pct + quality_health (from the same signal pipeline as signal.json), total_rounds (trajectory snapshot count), completed_at (ISO-8601), project_id. The polling primitive for integrators, status dashboards, and social bots that need to react to completions without subscribing to per-sim webhooks. ?limit=N (1–50, default 20; out-of-range values clamp silently). Same publish gate as /api/feed.rss (only is_public AND status="completed" sims appear). Distinct from /api/simulation/public (heavy filterable gallery), /api/feed.rss + /api/feed.atom (subscription-style XML feeds), and /api/status.json (liveness probe with no per-sim payload). No auth (public-keyless — added to the internal_auth_guard allow-list alongside /api/status.json). ETag derived from count + the newest entry's completed_at so If-None-Match short-circuits to 304. Cached 30 s. Empty deployment returns {count: 0, results: []}, never 404s
POST/api/simulation/<id>/articleGenerate a Substack-style write-up
GET/api/simulation/<id>/exportFull JSON export
GET/api/simulation/listList simulations
GET/api/simulation/historySimulation history / diffs
GET/api/simulation/publicFilterable, paginated public gallery feed

GET /api/simulation/public supports keyword + dominant-stance + quality-tier + outcome-label + sort filters so an analyst can pull "every excellent-quality bearish call about Aave" as one URL:

GET /api/simulation/public?q=aave&consensus=bearish&quality=excellent&sort=rounds&page=1
Query paramValuesNotes
qfree text, ≤200 charsCase-insensitive substring match against the scenario.
consensusbullish / neutral / bearishDominant final-round stance using the same ±0.2 threshold the share card / replay GIF / transcript / webhook / feed all use.
qualityexcellent / good / fair / poorCompared case-insensitively against the first word of quality_health.
outcomecorrect / incorrect / partialImplies verified=1 (verified-only).
sortdate / rounds / agents / trendingdate (default — newest first), rounds (highest current_round first), agents (largest population first), or trending (highest cumulative share-surface serve count first — sums every counter the surface-stats endpoint exposes).
verifiedtruthy (1/true/yes)Restrict to simulations with a recorded outcome annotation — the /verified hall.
limit / offset[1, 100] / ≥0Pagination knobs. total reflects the filtered count.
page≥11-based alternative to offset. Wins over offset when both are supplied.

Filters compose with logical AND. Empty / unknown values are no-ops, so ?consensus= returns the unfiltered listing and ?sort=popularity falls back to sort=date rather than 400-ing.

Analyst quickstart

Pull a simulation's per-round belief trajectory straight into Pandas:

import pandas as pd
df = pd.read_csv("https://your-host/api/simulation/<id>/trajectory.csv")
print(df.describe())
df[["round", "bullish_pct", "bearish_pct"]].plot(x="round")

Or via DuckDB / JSONL for streaming pipelines:

import duckdb
duckdb.sql("""
  SELECT round, bullish_pct
  FROM read_json_auto('https://your-host/api/simulation/<id>/trajectory.jsonl')
""").df()

The CSV column order is locked: round, round_timestamp, bullish_pct, neutral_pct, bearish_pct, participating_agents, total_posts, total_engagements, quality_health, participation_rate. The bullish / neutral / bearish percentages use the same ±0.2 stance threshold as the gallery, share card, transcript, webhook, and feed surfaces, so the numbers in the DataFrame match what every other surface reports for the same round.

Or skip the boilerplate entirely and download the pre-populated notebook — trajectory data embedded, belief-evolution + final-consensus charts scaffolded, ready to run:

curl -fsSL "https://your-host/api/simulation/<id>/notebook.ipynb" \
  -o simulation.ipynb
jupyter lab simulation.ipynb     # or: code simulation.ipynb, or upload to Colab

The notebook is self-contained — the trajectory CSV is embedded as a Python string literal so the cells run in an air-gapped kernel. Identical exports of a finished simulation produce bytewise-identical notebooks (citation-hash friendly), same property the reproduce.json blob has. nbformat 4 spec: https://nbformat.readthedocs.io/.

Polymarket trading-bot quickstart

A Polymarket bot can go from "simulation result" to "actionable YES / NO signal" in a single curl call — the polymarket.json endpoint is the adapter:

curl -s "https://your-host/api/simulation/<id>/polymarket.json" \
  | jq '{yes: .yes_probability, no: .no_probability, tier: .confidence_tier}'
import requests
sig = requests.get(f"https://your-host/api/simulation/{sim_id}/polymarket.json", timeout=10).json()
if sig["confidence_tier"] in ("confident", "high-conviction") and sig["risk_tier"] != "high-risk":
    place_order(market_id, side="YES" if sig["yes_probability"] > 0.5 else "NO",
                size=POSITION_SIZE_BY_TIER[sig["confidence_tier"]])

Stricter publish gate than signal.json: only sims with status == "completed" emit a payload. A 404 means the simulation is still running or has no recorded rounds — a bot should treat it as "not ready" and skip, not retry. The confidence_tier field is the four-bucket discrete scale (speculative / moderate / confident / high-conviction) bots use for position-sizing logic; the underlying confidence_pct is also returned for callers that want the continuous value. yes_probability + no_probability == 1.0 within float tolerance — the invariant a Polymarket order-book consumer expects.

oEmbed auto-unfurl

Most oEmbed consumers (Notion, Ghost, Substack, WordPress) discover the endpoint automatically from the <link rel="alternate" type="application/json+oembed"> tag on the /share/<id> page — paste the share link and the rich card appears with no further setup. To query the provider directly:

curl -s "https://your-host/oembed?url=https://your-host/share/<id>" \
  | jq '{type, title, thumbnail_url, html}'
# XML representation (some consumers, Notion among them, probe the text/xml+oembed link)
curl -s "https://your-host/oembed?url=https://your-host/share/<id>&format=xml"

Returns a type: "rich" payload — thumbnail_url$ \text{is} \text{the} 1200 \times 630 \text{share}-\text{card} \text{PNG} \text{and} $html$ \text{is} \text{an} 800 \times 500 \text{iframe} \text{over} $/embed/<id>. The url must be a share link on this deployment's own host; a foreign domain, an unpublished sim, or a missing sim all return 404. An unsupported format returns 501 per the oEmbed spec.

Search Engine Discoverability

Two infrastructure-tier endpoints make the public-simulation gallery discoverable to web search:

  • GET /sitemap.xml — auto-generated sitemap (sitemaps.org 0.9 schema). One <url> per published sim's /share/<id> page (priority 0.8) plus one per /watch/<id> page (priority 0.7). <lastmod> in W3C YYYY-MM-DD form. <changefreq>always</changefreq> for in-progress sims, weekly / daily for completed ones. Sorted by simulation_id so two consecutive renders against the same corpus are byte-identical. Returns 404 when ENABLE_SITEMAP=false. Cached public, max-age=3600.
  • GET /robots.txt — crawler directives. Always served. Disallow: /api/ keeps the JSON namespace out of the index; Allow: lines for /share/, /watch/, /explore, /verified, /embed/ invite crawlers into the public-discovery surfaces. When the sitemap is enabled, advertises it via the standard Sitemap: <PUBLIC_BASE_URL>/sitemap.xml directive.

Submission flow: in Google Search Console (or Bing Webmaster Tools / Yandex Webmaster / etc.), add the site once and submit https://<your-deployment>/sitemap.xml. Every newly published simulation lands in the next crawl — no per-sim manual step.

Opt-out: set ENABLE_SITEMAP=false in the deployment environment to make /sitemap.xml return 404 and drop the Sitemap: advertisement from robots.txt. Useful for private MiroShark instances or operator-only deployments where simulations should not surface in public search results.

The Embed dialog has a "🔍 Discoverable in web search" callout that confirms the simulation is in the sitemap (or explains how to enable it when disabled). The flag is exposed via GET /api/config/sitemap — public, no secret config leaked.

Report Agent

MethodPathPurpose
POST/api/report/generateLaunch ReACT report agent
POST/api/report/generate/statusPoll generation
GET/api/report/<id>Full report
GET/api/report/by-simulation/<sim_id>Report for a simulation
GET/api/report/<id>/downloadPDF export
POST/api/report/chatChat with report agent (re-queries graph)
GET/api/report/<id>/agent-logFull ReACT trace
GET/api/report/<id>/agent-log/streamSSE stream
GET/api/report/<id>/console-logRaw LLM call logs

Observability

MethodPathPurpose
GET/api/observability/events/streamSSE feed
GET/api/observability/eventsEvent log (paginated)
GET/api/observability/statsAggregate stats
GET/api/observability/llm-callsLLM call history

Settings & Push

MethodPathPurpose
GET / POST/api/settingsRuntime settings (masked keys)
POST/api/settings/test-llmPing configured LLM
GET/api/simulation/push/vapid-public-keyVAPID key for web push
POST/api/simulation/push/subscribeRegister a browser subscription
POST/api/simulation/push/testFire a test notification

Interactive Documentation

MethodPathPurpose
GET/api/docsSwagger UI rendered against this spec — try-it-out enabled
GET/api/openapi.yamlOpenAPI 3.1 spec, YAML form (canonical)
GET/api/openapi.jsonSame spec, JSON form (handy for openapi-generator)

The spec is committed to the repo at backend/openapi.yaml. A unit test (backend/tests/test_unit_openapi.py) walks every Flask route on every push and fails CI if the spec drifts away from the implementation.