ADR-058: The strategy crate split — submission is mechanism, degenbot-strategy owns the plane¶
Status: accepted (2026-09-21). Records the landed extraction (commits 442527b36, 5daa9d670, 8888d85fc, c8c56aab0) and the vocabulary ruling that preceded it (b3a3d91f5). Basis: ADR-055 (pending-transaction strategy seams), ADR-054 (frame evidence seams), ADR-057 (the strategy host), ADR-019 (strategy-vs-engine), and the executed design. The crate sources remain the last word on what the code does today.
Context¶
Before the split, degenbot-submission held two different things at once: the
submission channel (signer, dispatcher, monitor, ledger, submit) and the
backrun strategy that used it — the reaction pipeline, the frame feed, the
anchored DFS, gap quarantine, and the pending-tx driver all lived beside the
signer. The backrun arm’s decision layer and loop also straddled
degenbot-bot. Nothing owned “concrete strategies” as a concept, so the
umbrella crate was the only place a pure-Rust consumer could find one.
“Strategy” was also overloaded: ExecutionStrategy named the four-part
executor-contract seam, and PendingTxStrategy named the source-side reaction
contract, so the word could mean any of three different things.
Decision¶
D1 — Submission is mechanism only¶
degenbot-submission owns the channel and nothing strategy-shaped. Its
surface is the signer, fee pricing, transaction params, the bundle target, the
dispatcher, the monitor, the submission ledger, and submit; it names no
strategy. A caller hands it a candidate and a SubmissionTarget and reads a
SubmitOutcome.
D2 — degenbot-strategy owns the plane and the concrete compositions¶
The strategy-plane crate (442527b36) is the home for concrete strategy
compositions and the vocabulary they compose over. It is pyo3-free. The
backrun arm then moved in (5daa9d670): backrun.rs and backrun_engine.rs
from degenbot-bot; backrun_strategy.rs, backrun_driver.rs plus its
subtree, anchored_dfs.rs, frame_pipeline.rs, pending_tx.rs,
market_context.rs, gap_probe.rs, gap_quarantine.rs, and
gap_quarantine_journal.rs from degenbot-submission.
The dependency direction is strategy → capability crates. Capability
implementations stay in their own crates and are re-exported here, never
moved. No capability crate depends on degenbot-strategy; only the umbrella
degenbot crate, degenbot-python, and (by the same allowance) any operator
binary may.
D3 — Vocabulary rulings (b3a3d91f5)¶
The bare word “strategy” was reserved for the top-level composition only:
ExecutionStrategy→ExecutionAdapter(modulestrategy.rs→adapter.rs). The Python lift needed no class rename of its own: its pyclass (PyPayloadComposer, Python-visiblePayloadComposer) was never named after the seam, and the trait appeared in the lift only in doc comments (renamed with the trait). The four-part Encode / Probe / Assess / Fee seam (ADR-025 D2) is an executor-contract adapter, not a strategy.PendingTxStrategy→PendingTxReaction. The pending-transaction pipeline is the source-side contract behind backrun-type strategies, not a strategy.Strategy names a top-level kind of profit opportunity, composed as values over six capability slots: source, infrastructure, calculation, encoder, simulator, submission.
D4 — Per-ecosystem types, composition over sub-traits¶
The backrun arm split into two distinct types (8888d85fc): MevblockerBackrun
and TxpoolBackrun. Both parameterize the same reaction machinery (frame feed,
anchored discovery, decide gate, simulation, dispatch) by composition; they
differ only in the value of their SubmissionSlot:
Mevblocker { bundle_url, private_url }— aneth_sendBundleauction on the MEVBlocker searcher WebSocket, with the raw-broadcast fan-out leading with the private endpoint before the read-provider fallback.PublicFanOut { relays }— public-mempool relay fan-out with the read provider as fallback.
There is no Backrun sub-trait. Distinct ecosystems get distinct types;
shared reaction capabilities are mixed in by composition. Both compositions
are independently activatable and can run in one StrategyHost process.
D5 — The plane contract is extracted by subtraction¶
strategy_plane.rs (c8c56aab0) holds the surface the three concrete strategies
demonstrably share and nothing else: StrategyName (the facet name, the host
registration id, and the one select() path) and trait Strategy { const NAME }.
A capability slot with one consumer stays out of the plane. The hosted boot
registers via StrategyName::ALL in plane order, and the config resolves the
settlement broadcast endpoints through the same plane surface instead of a
second config copy.
Settlement keeps a special-case ledger in settlement.rs, cited as the
open-work map rather than normalizing the cases away:
Pump self-driving. The engine pump owns the settlement block clock, so the host registers no spawn factory and
start_drivingskips it; generalizing the pump is ADR-018’s on-demand work.Unconditional
Configuredregistration. The hosted boot registers the settlement facet as configured even whenstrategy.settlement.activeis false; the runner resolves its live-mode gate after the engine exists.Process-global submission lane. The settlement
NonceLaneis installed process-wide, because the settlement seam is the Python-driven arm of the one hosted process.Sole channel registrar.
EngineChannelHandles::register_onis the closureStrategyHost::minthands the exclusive&mut Hub.ADR-057 head-liveness coupling. The hosted head feed runs from the Python settlement consumer’s accepted-header clock, guarded on hosted activity.
D6 — Seam learnings from the exercise¶
What actually moved differed from the plan:
Tests and fixtures moved with their modules.
trace_*tests andbackrun_lane_e2e(fromdegenbot-bot) joinedanchored_dfs,frame_pipeline,admit_probe, the v3/v4 window suites,frame_liveness, andquarantine_journal_roundtripunder the new crate.hosted_driver_ambient_runtimeandmock_third_familystayed indegenbot-submission, because they exercisestrategy_hostplus the submission surface only. Two out-of-tree seams were blindly repointed to the moved paths: thedegenbot-configenv-read allowlist test and theframe_oraclecapture fixture path.A recorded conflict, resolved by carve-out. Gating settlement registration on
strategy.settlement.activewould have broken the pinned default-boot enable behavior (the runner resolves its live relay posture after the engine exists, and a dry-run boot must still enable the arm). The unconditional-Configuredcarve-out was kept and documented insettlement.rsrather than silently changed.The config hard cut.
strategy.backrunwas replaced bystrategy.mevblocker_backrunandstrategy.txpool_backruntyped facets at the single declaration site, with env spellingsDEGENBOT_STRATEGY_MEVBLOCKER_BACKRUN_*/DEGENBOT_STRATEGY_PEER_BACKRUN_*andStrategyNamebecomingSettlement | MevblockerBackrun | TxpoolBackrun. There are no aliases and no legacy parsing; readiness legality is per facet (MEVBlocker bid mode requireskey_fileandmevblocker_url). The retired single-armstrategy.nameselector stays undeclared, pinned bystrategy_arm_selector_is_retired.
Consequences¶
A pure-Rust consumer depends on
degenbot-strategyfor concrete strategies;degenbot-submissionno longer names one.Adding a strategy family is one composition type, one config facet, and one registration. The plane contract grows only when a shared slot gains a second consumer.
The retired names (
ExecutionStrategy,PendingTxStrategy) and the old crate homes survive only in ADR bodies and the historical record.