Skip to main content
[ DOCS · SCENARIOS ]

Scenarios

A scenario is a config file that describes who acts, what changes, and what should hold. A replay swaps the synthetic shock surface for recorded initial-state, trajectory, and oracle files. This page walks through the fields for both.

What Is A Scenario?

A scenario is one declared experiment you can run against a program. Where the adapter says what your program is, the scenario says what happens to it: who the actors are, what they do, what market conditions apply, and what starting conditions the world is in.

Think of it as a script for a controlled lab run. You name the setup, commit the seed, and let deterministic actors push on the program for a fixed number of ticks. When the run ends, Riptide reports a verdict, confidence, coverage checks, which invariants fired, where, and why — and emits a run collection plus an evidence pack you can rerun bit-for-bit.

What a scenario describes

  • Who acts — the personas (whales, LPs, liquidators, redeemers) and how many of each.
  • What happens in the world — price shocks, oracle trajectories, scheduled events.
  • Starting conditions — the seed or seed count, tick count, initial balances, and any scenario-specific parameters.
  • What to watch — the invariants from the adapter that this run cares about.

Why one run isn't proof

A scenario is reproducible evidence of one trajectory. The same seed gives you the same answer every time, which is exactly what makes it reviewable — but one seed doesn't cover every future market condition. That's what sweeps are for: run the same scenario across a grid of parameter values to map where your invariants hold and where they break.

tick 0tick NwhaleLPredeemerbad_debtinvariantsseed: 0x42...

Scenario Shape

Concretely, a scenario is a run-config.json that names the scenario, seed or seed count, ticks, agents, personas, output path, optional adapter path, and any scenario-specific parameters. The CLI discovers .riptide/scenarios/**/run-config.json from the current directory and uses the directory path as the stable scenario name.

riptide list
riptide run --adapter .riptide/adapters/<your-program>.toml --harness .riptide/harness
riptide run .riptide/scenarios/<name>/run-config.json --adapter .riptide/adapters/<your-program>.toml --seeds 10
riptide run fixtures/scenarios/<path>/run-config.json

With no positional argument, riptide run runs every discovered scenario. A positional argument that points to an existing .json file runs that file directly; otherwise the argument is treated as a glob over discovered scenario names. Use riptide list before CI wiring when you want to see exactly what the current repo will run.

Seeds

The same seed plus the same committed inputs should reproduce the same output. One seed is one declared experiment, not a broad review result; use sweeps and compare the regions where invariants hold or fire.

A run config with "seed" pins one deterministic cell. A run config with "seeds" asks Riptide to run that many seeds as a sweep. If neither the config nor CLI sets a seed count, Riptide treats the config as seedless and runs the default 50-seed confidence sweep from a generated seed root.

{
  "scenario": "baseline",
  "adapter": "../../adapters/ammv2.toml",
  "agents": 1000,
  "ticks": 30,
  "seeds": 3,
  "personas": ["swapper", "arbitrageur", "lp-provider"]
}

Use --seeds <N> to override the scaffolded count and --seed-root <N> when you want a reproducible sweep seed stream without editing the file.

Actors And Personas

Personas model economic actors such as whales, liquidity providers, arbitrageurs, liquidators, stablecoin redeemers, or withdrawal-queue participants. In the engine, these are deterministic behavior policies over shared state. Scenario configs reference persona slugs from the adapter's inline [personas.*] blocks; there is no separate .riptide/personas/ directory in new scaffolds.

For the persona archetypes available to /riptide-config and the advanced init wizard, grouped by protocol class — plus the authoring guide for writing your own — see Personas.

Market Dynamics

Engine-level scenario implementations currently ship for baseline runs and lending oracle price shocks. Other stress shapes belong in replay fixtures or hand-authored configs until matching engine scenario implementations land.

Replay Surface

A scenario describes a synthetic experiment. A replay swaps the synthetic shock surface for recorded inputs:

  • initial-state.json — account values to seed before tick 0.
  • trajectory.json — the explicit per-tick action sequence to execute.
  • oracle-trajectory.json — the explicit oracle-write sequence.
  • Replay-scoped adapter optional — a sibling adapter override used when the recorded program shape differs from the one your repo currently builds.

Replays use the same engine, the same canonical-hash discipline, and the same pack format as scenarios. They differ only in what drives the program — recorded inputs, not deterministic personas + a shock config.

Use synthetic scenarios to explore parameter regions. Use replays when you need to preserve a specific trajectory and rerun it byte-for-byte in review or CI.

Invariant Checks

Invariants live in the adapter and are evaluated during runs. A result should name invariant firings and the first tick where possible, so a reviewer can inspect the boundary instead of reading a vague failure.

Sweeps

Seed sweeps turn "what happens if this random path changes" into a map. Riptide reports planned work up front, for example 1000 agents x 30 ticks = 30,000 events/seed, then prints progress, active workers, average seed runtime, and ETA while the sweep runs. Served runs open the collection dashboard so the selected sweep can be reviewed before drilling into one scenario's metrics, events, invariant rows, and coverage checks.

riptide run --adapter .riptide/adapters/<your-program>.toml --harness .riptide/harness --serve
riptide run .riptide/scenarios/<name>/run-config.json --adapter .riptide/adapters/<your-program>.toml --seeds 10 --seed-root 4242

Larger sweeps are real work. A 1000-agent, 30-tick, 50-seed run schedules 1,500,000 planned events before account reads, instruction dispatch, invariant checks, and harness startup costs.

One-Seed Boundary

Riptide produces simulation evidence for declared runs. Treat a scenario as one declared experiment with reproducible inputs — not as a coverage claim across all configs, and not as an oracle for all future market conditions.

Writing A Custom Scenario

The engine ships two Scenario impls today — BaselineScenario (no perturbation) and PriceShockScenario (used by both the price-shock and bank-run presets via preset TOML and env-var tuning). The scenario field in run-config.json is a key into a Rust dispatch table at engine/src/main.rs, not a config-driven extension point. If you need a new perturbation shape, work through these three paths in order.

Path A — Config-only

Preferred when it works. Tune the existing price-shock impl with the RIPTIDE_PRICE_SHOCK_DROP env var (fractional drop applied when the shock fires; overrides the preset value), or write a new preset under fixtures/scenarios/presets/<name>.toml with kind = "price_shock" and your own shock_tick, drop, and starting-price values. No code change.

Path B — Replay

For arbitrary trajectories. If the dynamic can be expressed as a recorded sequence, author initial-state.json, trajectory.json, and oracle-trajectory.json, then run riptide replay. The engine treats replays as data — no Rust required. See Replay artifacts.

Path C — Fork the engine

For genuinely new perturbation logic that neither preset tuning nor a replay can express:

  • Fork riptide on GitHub.
  • Add engine/src/scenario/<your_scenario>.rs implementing the Scenario trait.
  • Register it in the dispatch table at engine/src/main.rs.
  • Run cargo build --release to rebuild the engine binary.
  • Point the CLI at the custom binary via RIPTIDE_ENGINE_BIN=/path/to/your/riptide-engine.

This leaves you on a fork until the impl is upstreamed.

Try Path A → Path B → Path C, in that order.