Mayavi
Platform & pipeline·5 min read

The 21-Bundle Multichain Matrix: 6 Chains × 4 Protocols, One Pipeline

Twenty-one channel-agnostic artifact bundles, one Typer command per scenario, one shape-aware HTML report template. The pipeline that turns a scenario YAML into a publishable artifact in two CLI invocations and zero manual editing.

Part of the Platform & pipeline series.

Posts 4–7 walked the four protocol adapters. This post walks what happens when you run all four across the six chains they each support, render every result through the same shape-aware HTML report template, and ship the artifacts as a single deployable corpus. The 21-bundle matrix isn't a marketing screenshot — it's the output shape of the engine's bundle pipeline.

What "channel-agnostic" actually means

A scenario run produces three things:

  1. Run digesteval.json, a machine-readable summary of the scenario's identity (chain, protocol, fork block, horizon), headline metrics, and KPI tiles.
  2. Human reportreport.html, a self-contained Plotly + Jinja2 page with the scenario's KPIs, action timeline, and per-step state evolution.
  3. (Optional) marketing assetsog.png (1200×630 OG preview), screencast.webm (Playwright-rendered scripted screencast). Skipped on hosts without Chromium installed.

Together those four files make up a "bundle." docs/artifacts/protocols/<bundle-slug>/ is the channel-agnostic output of the engine — not "outreach content," not "marketing copy," just the engine's deterministic per-scenario output. Embed it in a blog post (every Mayavi blog post does), open the HTML in any browser, JSON-parse the eval into a different visualization, embed via iframe into a third-party page — same artifact, different channels.

The 21 bundles, all 2026-05-14

The current matrix (run + rendered on 2026-05-14, committed to the repo at docs/artifacts/protocols/):

ProtocolChains coveredScenarios per chainBundle count
Aave V3mainnet, BNB, Polygon, Arbitrum, Base, Optimismborrower + USDC depeg cascade6 × 2 = 12
Compound V3mainnet, Arbitrum, Base, Polygonborrower4 × 1 = 4
Curve StableSwapmainnetdepeg1 × 1 = 1
SparkLendmainnetborrower1 × 1 = 1
Uniswap V3 (special scenarios)mainnetcliff-collision, ENA launch replay, vesting-cliff WETH3 × 1 = 3
Total21

Optimism's Compound V3 deployment is the matrix gap — deferred to Phase 6. The release gate _MIN_COMMITTED_ARTIFACTS = 21 reflects shipped reality, not aspiration; raising it to 22 requires shipping the Optimism Comet bundle in the same PR that ratchets the floor.

One bundle from each protocol's column, embedded inline:

Bundle: aave-v3-mainnet-borrower-2026-05-14Aave V3 (mainnet) borrower
Bundle: compound-v3-mainnet-borrower-2026-05-14Compound V3 (mainnet) borrower
Bundle: curve-mainnet-depeg-2026-05-14Curve StableSwap (mainnet) depeg
Bundle: spark-mainnet-borrower-2026-05-14SparkLend (mainnet) borrower

The four bundles above are all rendered from the same Jinja2 template, just with different scenario-type detection driving the KPI tile selection. Borrower-shape scenarios show HF, debt, collateral, liquidation events. Depeg-shape scenarios show oracle price track + cascade fan-out. Vesting-shape scenarios show inventory burn + realized USDC. The shape detection happens in mayavi/report/builder.py at render time — one template, four shape branches, zero per-scenario customization.

How a bundle gets built

Two CLI invocations:

# 1. Run the scenario. Forks the chain at the pinned block, runs the
#    agent population through the configured horizon, persists the run
#    + per-step actions + market snapshots to DuckDB.
uv run mayavi run mayavi/scenarios/aave_borrower.yaml
# -> persisted run_id=abc123... to data/runs/runs.duckdb
 
# 2. Render the run as a bundle. Pulls the run from DuckDB, renders
#    report.html via the shape-aware template, emits eval.json, optional
#    og.png / screencast.webm if Chromium is available.
uv run mayavi artifact abc123 --out docs/artifacts/protocols/
# -> docs/artifacts/protocols/aave-v3-mainnet-borrower-<date>/

The slug auto-derives from <protocol>-<chain>-<scenario_type>-<date>. The --out directory + slug auto-place the bundle; <bundle-slug>/index.md's table row (the master catalogue at docs/artifacts/protocols/index.md) is appended idempotently — re-running on the same run-id replaces the row, not appends a duplicate.

Regenerating the whole matrix

bash scripts/generate_all_artifacts.sh
# Runs every scenario in scenarios/registry.py against its configured
# fork block + chain, then renders each as a bundle. ~10 minutes on a
# warm fork cache; ~30 minutes cold.

This is the script the founder runs at the close of every Phase. The 2026-05-14 commit timestamp on all 21 bundles is when that batch ran. Re-running on a different host reproduces every bundle bit-identically modulo the per-bundle generation timestamp, because the engine is deterministic (Post 14 is the next one in this theme).

Per-chain RPC env-vars (ALCHEMY_RPC_URL for mainnet, ALCHEMY_<CHAIN>_RPC_URL for the others) are the only configuration that needs to differ across hosts. BNB requires a paid Alchemy plan (free tier returns HTTP 403); every other chain works on free-tier. The script auto-skips chains whose env-var is unset rather than failing the whole run.

Why the 4-pillar discipline lives at the bundle layer

Every bundle's eval.json carries the four pillars of the original Phase-2 release-gate contract baked into the run digest:

{
  "scenario": {
    "name": "...",
    "protocol": "aave-v3",
    "chain": "mainnet",
    "chain_id": 1,
    "fork_block": 19000000,
    "horizon_steps": 4,
    "seed": 42
  },
  "headline": { "value": "4", "label": "simulation steps against deployed protocol contracts" },
  "run_seconds": 1.641119665990118,
  "trained_agent_artifacts": [
    { "agent": "...", "file": "docs/artifacts/aave_ppo_v2_local_2026-05-07.json", "headline": "..." }
  ]
}

The scenario.chain_id field is the artifact-side enforcement of Post 4's multichain-dispatch correctness: a re-rendered bundle whose chain_id doesn't match its slug is a bug. The seed field is the determinism gate's artifact-side declaration — same seed + same fork_block = same bundle, byte-identical to the prior render. The run_seconds field is the lightweight performance signal — a regression that 10×'s the runtime would surface here before any user-facing perception of slowness.

What this enables for downstream channels

Blog: every Mayavi blog post embeds bundles by slug — <ReportEmbed bundle="aave-v3-mainnet-borrower-2026-05-14" /> in the MDX, the Next.js sync-artifacts step makes them available at build time. No marketing screenshots; the live HTML render is the post's evidence.

Dashboard: app.mayavi.sambhal-labs.com serves the same bundle HTMLs through the FastAPI proxy at /api/runs/<id>/report (run-history side) and through the static /artifacts/ route (blog side). One backend → two surfaces.

PDF: the dashboard's "Download PDF" button runs Playwright over report.html server-side and streams the PDF. Same artifact, third channel.

Future channels (deck, RSS-feed-of-runs, embed-iframe-on-third-party-site, …) are all bundle-shape-compatible because the bundle is the API. Adding a channel doesn't touch the engine.

What it does NOT enable

The matrix is breadth at the cost of depth. Each bundle today runs a small, illustrative scenario (4–10 horizon steps; one borrower or five). The bundle pipeline isn't where you do a 10,000-step adversarial multi-agent run; it's where you turn that run, after it's been validated by the gym/RL surface (Post 8), into a publishable artifact. The pipeline is for publishing runs, not for exploring them; exploration happens in mayavi run + ad-hoc DuckDB queries.

Where this leaves the platform theme

Post 14 explains the property that makes any of this re-runnable: the engine's determinism guarantee. Post 15 walks how all of this lands on the deployed dashboard — Modal + Vercel + DuckDB + the bearer-token leak we caught and fixed.