checking system…
Docs / API Reference
REST endpoints, WebSocket channels, Redis streams + envelope shapes.

MAF — API Reference

Three surfaces:

  1. REST endpoints — the dashboard /api/*.
  2. WebSocket channels — realtime event feed.
  3. Redis streams — the data + control plane.

Each endpoint name links to its handler in src/maf/dashboard/routers/ — the dashboard is split across one router module per concern. A handful of routes (data sources sampler, app config) still live in src/maf/dashboard/api.py.


REST endpoints

Arenas

Method · Path Handler Returns
GET /api/arenas list_arenas All arenas with schedule + source count
GET /api/arenas/{name} get_arena Full arena config
GET /api/arenas/{name}/config get_arena_config Raw arena config (with ETag header for optimistic locking)
PUT /api/arenas/{name}/config update_arena_config Save edited YAML — pydantic-validated (422), If-Match-guarded (412), atomic write
GET /api/arenas/{name}/last_decision get_arena_last_decision Latest trail entry summary
GET /api/arenas/{name}/trail list_trail Recent trail entries
GET /api/arenas/{name}/trail/{id} get_trail_entry Full trail entry
POST /api/arenas/{name}/run run_arena Dispatch a synchronous arena run
POST /api/arenas/{name}/backtest run_backtest Backtest a trading arena over a date range
GET /api/arenas/{name}/freshness arena_freshness Per-binding freshness for the Setup tab badges
GET /api/arenas/{name}/config example

Response headers include ETag: <16-hex> — pass it back on PUT as If-Match: <value> to get optimistic-locking. PUT without If-Match skips the concurrency check.

PUT /api/arenas/{name}/config outcomes
Status Meaning
200 Saved. Response body includes the new ETag.
404 Arena YAML file not found.
412 If-Match header doesn't match current ETag — someone else saved while you were editing.
422 Pydantic validation failed. Body's detail field has the field path + reason. File is untouched.
500 Atomic write failed. Tempfile is cleaned up; file is untouched.
POST /api/arenas/{name}/run example

Request:

Readable view
target
tickerNVDA
Show raw JSON
{
  "target": {"ticker": "NVDA"}
}

Response (truncated):

Readable view
arenamarket_pulse
trail_id20260516_094822_312
signalBUY
agent_signals
[0]
agentprice_analyst
signalBULLISH
confidence0.72
......
synthesis
score0.32
verdictBUY
confidence0.61
reasoning...
source_metrics(empty list)
reports
price_analyst...
news_analyst...
decisions
synthesis_agentBUY
trace(empty object)
target
tickerNVDA
Show raw JSON
{
  "arena": "market_pulse",
  "trail_id": "20260516_094822_312",
  "signal": "BUY",
  "agent_signals": [{"agent": "price_analyst", "signal": "BULLISH", "confidence": 0.72, "...": "..."}],
  "synthesis": {"score": 0.32, "verdict": "BUY", "confidence": 0.61,
                 "reasoning": "..."},
  "source_metrics": [],
  "reports": {"price_analyst": "...", "news_analyst": "..."},
  "decisions": {"synthesis_agent": "BUY"},
  "trace": {},
  "target": {"ticker": "NVDA"}
}

GET /api/arenas/{name}/freshness example
Readable view
arenamarket_pulse
now_ms1778948000000
bindings
[0]
namebars_1m_target
adaptertrtools2_bars
source_typestream
streamtrtools2:bars:1m
length80
age_seconds5.4
statuslive
[1]
namekronos_forecast_1m
adapterkronos_forecast
source_typekey
length1
age_seconds12.0
statuslive
emit_streamkronos:forecasts:emitted
[2]
namehistory_bars
adaptertrtools2_api
source_typeexternal
detailHTTP API — trtools2 dashboard (default :8888)
statusexternal
Show raw JSON
{
  "arena": "market_pulse",
  "now_ms": 1778948000000,
  "bindings": [
    {
      "name": "bars_1m_target",
      "adapter": "trtools2_bars",
      "source_type": "stream",
      "stream": "trtools2:bars:1m",
      "length": 80,
      "age_seconds": 5.4,
      "status": "live"
    },
    {
      "name": "kronos_forecast_1m",
      "adapter": "kronos_forecast",
      "source_type": "key",
      "length": 1,
      "age_seconds": 12.0,
      "status": "live",
      "emit_stream": "kronos:forecasts:emitted"
    },
    {
      "name": "history_bars",
      "adapter": "trtools2_api",
      "source_type": "external",
      "detail": "HTTP API — trtools2 dashboard (default :8888)",
      "status": "external"
    }
  ]
}

Status values: live (age < 5 min), stale (5 min – 1 h), old (≥ 1 h), empty (stream exists but no entries), missing (cache/stream absent), external (HTTP/SQL, no caching), request_response (round-trips through Redis), unknown.

Triggers

Method · Path Handler Returns
GET /api/triggers/library triggers_library Prebuilt trigger rules loaded from config/trigger_templates.yaml. Hot-reloads on mtime change.
POST /api/triggers/validate triggers_validate Run a when: expression through safe_eval against an optional sample payload. Returns {ok, result, error}.
POST /api/triggers/validate example

Request:

Readable view
exprabs(payload.prob_up_delta) > 0.15 or payload.direction_flipped
sample_payload
prob_up_delta0.22
direction_flippedfalse
Show raw JSON
{
  "expr": "abs(payload.prob_up_delta) > 0.15 or payload.direction_flipped",
  "sample_payload": {"prob_up_delta": 0.22, "direction_flipped": false}
}

Response (success):

Readable view
oktrue
resulttrue
errornull
Show raw JSON
{"ok": true, "result": true, "error": null}

Response (parse error):

Readable view
okfalse
resultnull
errorsafe_eval: syntax: invalid syntax
Show raw JSON
{"ok": false, "result": null, "error": "safe_eval: syntax: invalid syntax"}

Channels

Method · Path Handler Returns
GET /api/channels api_channels Categorised list of every Redis Stream
GET /api/channels/preview?stream=…&count=25 api_channel_preview Decoded entries + inferred schema
GET /api/channels example
Readable view
channels
[0]
streamkronos:forecasts:emitted
categoryderived
labelKronos — forecasts emitted
descriptionCompact emit per (symbol, timeframe)…
sourcecatalog
length12
last_id1778923456789-0
categories[arena_output, derived, input, control]
Show raw JSON
{
  "channels": [
    {
      "stream": "kronos:forecasts:emitted",
      "category": "derived",
      "label": "Kronos — forecasts emitted",
      "description": "Compact emit per (symbol, timeframe)…",
      "source": "catalog",
      "length": 12,
      "last_id": "1778923456789-0"
    }
  ],
  "categories": ["arena_output", "derived", "input", "control"]
}
GET /api/channels/preview example
Readable view
streamkronos:forecasts:emitted
count5
entries
[0]
stream_id1778923456789-0
ts_ms1778923456789
fields
symbolNVDA
directionNEUTRAL
prob_up0.27
prob_up_delta-0.23
direction_flippedfalse
modelNeoQuasar/Kronos-small
schema
[0]
namesymbol
types
string5
presence5
samples[NVDA]
is_nestedfalse
Show raw JSON
{
  "stream": "kronos:forecasts:emitted",
  "count": 5,
  "entries": [
    {
      "stream_id": "1778923456789-0",
      "ts_ms": 1778923456789,
      "fields": {
        "symbol": "NVDA",
        "direction": "NEUTRAL",
        "prob_up": 0.27,
        "prob_up_delta": -0.23,
        "direction_flipped": false,
        "model": "NeoQuasar/Kronos-small"
      }
    }
  ],
  "schema": [
    {"name": "symbol", "types": {"string": 5}, "presence": 5, "samples": ["NVDA"], "is_nested": false}
  ]
}

Watch list

Method · Path Handler Behaviour
GET /api/watch?kind=symbol list_watch Current watch entries
POST /api/watch add_watch Add an entry (extends TTL on repeat)
DELETE /api/watch/{target_id}?kind=symbol remove_watch Remove every entry matching id (and optionally kind)
POST /api/watch example

Request:

Readable view
target_idNVDA
kindsymbol
ttl_seconds21600
attrs
horizon1m
Show raw JSON
{
  "target_id": "NVDA",
  "kind": "symbol",
  "ttl_seconds": 21600,
  "attrs": {"horizon": "1m"}
}

Response:

Readable view
oktrue
target_idNVDA
kindsymbol
expires_at1778945123.4
Show raw JSON
{"ok": true, "target_id": "NVDA", "kind": "symbol", "expires_at": 1778945123.4}

System

Method · Path Handler Returns
GET /api/system/status[?force=1] system_status Live connectivity checks: redis, ollama, trtools2, fomo2, mirofish, kronos_refresher, mirofish_refresher. Cached 5 s; ?force=1 bypasses cache.
GET /api/streams/health streams_health Length + age per known stream
GET /api/data/sources list_data_sources All source bindings across arenas + adapter catalog
GET /api/data/sources/{arena}/{name}/sample data_source_sample One-shot adapter fetch — useful for "what shape does this return?"
GET /api/system/status example
Readable view
cached_at1778948696.5
all_okfalse
checks
[0]
nameredis
oktrue
detailredis://localhost:6379/0
latency_ms3.4
[1]
nameollama
oktrue
detail39 models
latency_ms240.5
[2]
nametrtools2
oktrue
detailengine.db.questdb importable
latency_ms0.2
[3]
namefomo2
oktrue
detailfomo2 package importable
latency_ms0.2
[4]
namemirofish
oktrue
detailhttp://localhost:5101 200
latency_ms12.1
[5]
namekronos_refresher
oktrue
detailalive · 4s ago · 3 watched
latency_ms1.1
[6]
namemirofish_refresher
okfalse
detailnot running (no heartbeat — start `python -m maf` for service mode)
latency_ms0.9
Show raw JSON
{
  "cached_at": 1778948696.5,
  "all_ok": false,
  "checks": [
    {"name": "redis",              "ok": true,  "detail": "redis://localhost:6379/0", "latency_ms": 3.4},
    {"name": "ollama",             "ok": true,  "detail": "39 models",                "latency_ms": 240.5},
    {"name": "trtools2",           "ok": true,  "detail": "engine.db.questdb importable", "latency_ms": 0.2},
    {"name": "fomo2",              "ok": true,  "detail": "fomo2 package importable",     "latency_ms": 0.2},
    {"name": "mirofish",           "ok": true,  "detail": "http://localhost:5101 200",    "latency_ms": 12.1},
    {"name": "kronos_refresher",   "ok": true,  "detail": "alive · 4s ago · 3 watched",   "latency_ms": 1.1},
    {"name": "mirofish_refresher", "ok": false, "detail": "not running (no heartbeat — start `python -m maf` for service mode)", "latency_ms": 0.9}
  ]
}

LLM

Method · Path Handler Returns
GET /api/llm/rankings?category=programming api_llm_rankings OpenRouter usage leaderboard joined against Ollama Cloud catalog
GET /api/llm/picker?profile=synthesis api_llm_picker What the smart picker would choose for this profile right now

Modules + sources catalog

Method · Path Handler Returns
GET /api/modules list_modules All loaded data modules with config + adapter counts
PUT /api/modules/{name} update_module_config Edit module config in default.yaml — requires restart
GET /api/sources list_sources Every registered source adapter
POST /api/sources/test test_source Instantiate an adapter with given params and call fetch()

Specialised arena outputs

Method · Path Handler Returns
GET /api/oracle/envelopes?count=20 list_oracle_envelopes Latest crowd-simulation envelopes from maf:arena:crowd_simulation:output
GET /api/mastermind/decisions?count=20 list_mastermind_decisions Latest Decision envelopes from maf:arena:mastermind:output

Wizard

Method · Path Handler Returns
POST /api/wizard wizard_create Plan + scaffold a new arena from a free-text description

Docs viewer

Method · Path Handler Returns
GET /docs page_docs_index This documentation site
GET /docs/{slug} page_doc Rendered markdown doc (with src: links rewritten to /source/...)
GET /source/{path:path}?line=N page_source Source file viewer with line anchors + Pygments highlight
GET /docs.css docs_css Pygments stylesheet for code highlighting

Pages

All HTML pages render templates from src/maf/dashboard/templates/. Every <script> block on these pages is run through node --check in CI (test) so a stray ternary can't break onclick handlers without failing the build.

Path Page
GET / Dashboard — arena cards + run dialog
GET /live Live event feed
GET /channels Channels browser
GET /data Data + streams health
GET /llm LLM picker × rankings
GET /modules Modules config
GET /sources Sources catalog
GET /wizard Arena wizard
GET /mastermind Mastermind decisions feed
GET /oracle Crowd-sim envelopes feed
GET /arenas/{name} Per-arena detail page (Run / Setup / Backtest / Sources / History / Memory / Raw YAML tabs)
GET /docs Docs index
GET /docs/{name} One rendered doc
GET /more 308 redirect to / (legacy)
GET /favicon.ico 204 No Content (silences browser noise)

WebSocket channels

Both endpoints push JSON messages — one event per message.

Path Filter Source
/ws/events None — all events ws_events
/ws/arenas/{name} payload.arena == name ws_arena

Message shape

Readable view
kindphase.complete
arenamarket_pulse
arena_idabc-123
phaseanalysis
ts2026-05-16T09:48:22.123+00:00
correlation_idmanual-1
payload
patternparallel
agents[price_analyst, news_analyst, vol_regime_analyst, kronos_specialist]
elapsed_s12.4
stream_id1778923456789-0
Show raw JSON
{
  "kind": "phase.complete",
  "arena": "market_pulse",
  "arena_id": "abc-123",
  "phase": "analysis",
  "ts": "2026-05-16T09:48:22.123+00:00",
  "correlation_id": "manual-1",
  "payload": {
    "pattern": "parallel",
    "agents": ["price_analyst", "news_analyst", "vol_regime_analyst", "kronos_specialist"],
    "elapsed_s": 12.4
  },
  "stream_id": "1778923456789-0"
}

Heartbeats ({"kind": "ws.heartbeat"}) are sent every ~5 s when there are no new events so clients can detect dead connections.


Redis streams

Input streams (MAF reads from these)

Stream / key Producer Adapter
trtools2:bars:1m trtools2 feed engine Trtools2BarsSource
trtools2:bars:1h trtools2 feed engine same, different timeframe config
trtools2:news trtools2 news ingester Trtools2NewsSource
trtools2:indicators trtools2 indicator pipeline Trtools2IndicatorsSource
trtools2:strategy:events trtools2 strategy engine Trtools2StrategyEventsSource
trtools2 HTTP API (port 8888) trtools2 dashboard Trtools2ApiSource — uses TT2_API_KEY
Any MCP server (HTTP/SSE) external MCPSource — generic; bind once per (url, tool)
https://mcpv2.eodhd.dev/v1/mcp EODHD EODHDSource — uses EODHD_API_KEY, 77 tools (fundamentals, earnings calendar, news, EOD bars, ...)
fomo2:enriched fomo2 enrichment pipeline Fomo2StreamSource
fomo2:reports fomo2 LLM goal pipeline drives MirofishRefresher
fomo2:requests:in (MAF writes) on-demand fomo2 calls
fomo2:requests:out fomo2 sidecar replies Fomo2RequestSource

Derived streams + cache keys (MAF writes these)

Stream / key Producer Consumer
kronos:forecast:{sym}:{tf} (string key) KronosRefresher KronosForecastSource
kronos:forecasts:emitted KronosRefresher trigger dispatcher, dashboard
mirofish:sim:{report_id} (string key) MirofishRefresher MirofishSimSource
mirofish:sims:emitted MirofishRefresher trigger dispatcher
maf:actions:out trading arenas via ActionOutbox ActionConsumer
maf:decisions:out non-trading arenas via DecisionOutbox dashboards
maf:executions:out ActionConsumer ExecutionHarvester
maf:outcomes:out engine (when wired) ExecutionHarvester
maf:arena:{name}:output per-arena emit phase dashboards
maf:refresher:kronos:heartbeat (string key, TTL'd) KronosRefresher._write_heartbeat system_status
maf:refresher:mirofish:heartbeat (string key, TTL'd) MirofishRefresher._write_heartbeat system_status

Control streams

Stream Purpose Producer Consumer
maf:control:in Inbound commands ControlClient, dashboard, dispatcher ControlInbox
maf:control:out Acks keyed by correlation_id ControlInbox ControlClient.send()
maf:events Lifecycle events (best-effort) EventBus dashboard /ws/events, telemetry

Control commands

The data field of each maf:control:in entry is JSON:

Readable view
commandrun_arena
correlation_idabc
args
arenamarket_pulse
target
tickerNVDA
action_modemanual
emit_actiontrue
Show raw JSON
{
  "command": "run_arena",
  "correlation_id": "abc",
  "args": {
    "arena": "market_pulse",
    "target": {"ticker": "NVDA"},
    "action_mode": "manual",
    "emit_action": true
  }
}

Supported commands:

Command Args Result
run_arena {arena, target, action_mode?, emit_action?} {arena_id, signal, synthesis_verdict, …}
configure_arena {arena, selected_analysts?, max_iterations?} {changed: {...}}
set_data_source {arena, name, adapter, config} {arena, source, adapter}
reload_config {} {before, after}
health {} {arenas, modules, redis_url, streams}
list_arenas {} {arenas: [{name, description, schedule, phases, sources}, …]}

See ControlInbox.COMMANDS for the allow-list. Unknown commands return an explicit NACK ack rather than silent drop.

Envelope conventions

Almost every stream uses the same wire shape: one Redis Stream entry, one field named data, value is a JSON string. Consumers JSON-decode the data field and (often) validate against a Pydantic envelope. This keeps the wire schema-naive — consumers don't have to learn which Redis fields map to which envelope keys.

The exception: trtools2:bars:1m and friends use multi-field entries (one field per OHLCV column) because that's the existing trtools2 convention. The adapters handle both.