@@ -63,10 +63,14 @@ pub struct Config {
6363 /// set `provider = "turnkey" | "mpcvault"` to sign via an MPC wallet.
6464 #[ serde( default ) ]
6565 pub signer : Option < crate :: signer:: SignerConfig > ,
66- /// RFQ responder (dual-run pilot). Omitted → the responder never spawns and
67- /// nothing else changes. See [`RfqConfig`].
66+ /// RFQ responder. Omitted → the responder never spawns. See [`RfqConfig`].
6867 #[ serde( default ) ]
6968 pub rfq : Option < RfqConfig > ,
69+ /// Post resting orders on the public ladder. Default true (historical).
70+ /// Set false for RFQ-only: the bot answers private quotes and does not
71+ /// rest orders on the book. Spreads and liquidity still size RFQ.
72+ #[ serde( default = "default_true" ) ]
73+ pub book_enabled : bool ,
7074 /// Raw experimental gates. Values are uninterpreted strings on purpose:
7175 /// each consumer matches its own exact token, so a typo fails closed
7276 /// instead of enabling something adjacent.
@@ -75,6 +79,10 @@ pub struct Config {
7579 pub pools : Vec < PoolConfig > ,
7680}
7781
82+ fn default_true ( ) -> bool {
83+ true
84+ }
85+
7886/// The `[rfq]` block: connection details for the venue's maker quote stream.
7987/// The maker API key is NOT here — it comes from the environment variable
8088/// named by `api_key_env`, mirroring how the wallet key stays out of the file.
@@ -140,12 +148,24 @@ pub struct ExperimentalConfig {
140148 /// anything else (or omitted) keeps the card hidden.
141149 #[ serde( default ) ]
142150 pub rfq_panel : Option < String > ,
151+ /// RFQ-as-default rollout. Exact token [`RFQ_DEFAULT_GATE`] only. When
152+ /// set (here or in the fleet `panel.toml`), new bots start RFQ-only,
153+ /// book bots see a migrate-to-RFQ nudge, and Connect writes RFQ-only.
154+ #[ serde( default ) ]
155+ pub rfq_default : Option < String > ,
143156}
144157
145158/// The one token that unlocks the panel's RFQ beta card. Exact, case-sensitive
146159/// match — anything else (including a different case) fails closed.
147160pub const RFQ_PANEL_GATE : & str = "enable-rfq-beta" ;
148161
162+ /// The one token that turns RFQ into the default quoting mode. Exact match.
163+ pub const RFQ_DEFAULT_GATE : & str = "enable-rfq" ;
164+
165+ /// Filename for the fleet-wide flag, dropped next to the per-bot folders
166+ /// (`{bots_dir}/panel.toml`). Same `[experimental]` keys as a bot config.
167+ pub const PANEL_FLAGS_FILE : & str = "panel.toml" ;
168+
149169#[ derive( Debug , Clone , Deserialize ) ]
150170pub struct FeedConfig {
151171 /// HTTP endpoint returning `{ price, timestamp }`.
@@ -551,19 +571,29 @@ impl Config {
551571 }
552572
553573 /// True when the RFQ responder should run: the master switch is on and
554- /// there is at least one pool to quote. Anything less is a no-op — the
555- /// ladder never changes either way .
574+ /// there is at least one pool to quote. The public ladder is a separate
575+ /// switch ([`Self::book_enabled`]) .
556576 pub fn rfq_active ( & self ) -> bool {
557577 self . rfq . as_ref ( ) . is_some_and ( |r| r. enabled ) && !self . pools . is_empty ( )
558578 }
559579
560- /// True only for the exact [`RFQ_PANEL_GATE`] token. Read-only surface for
561- /// the panel; nothing in the bot keys off it.
580+ /// True only for the exact [`RFQ_PANEL_GATE`] or [`RFQ_DEFAULT_GATE`]
581+ /// token. Read-only surface for the panel; nothing in the bot keys off it.
562582 pub fn rfq_panel_unlocked ( & self ) -> bool {
583+ self . rfq_default_unlocked ( )
584+ || self
585+ . experimental
586+ . as_ref ( )
587+ . and_then ( |e| e. rfq_panel . as_deref ( ) )
588+ == Some ( RFQ_PANEL_GATE )
589+ }
590+
591+ /// True only for the exact [`RFQ_DEFAULT_GATE`] token on this config.
592+ pub fn rfq_default_unlocked ( & self ) -> bool {
563593 self . experimental
564594 . as_ref ( )
565- . and_then ( |e| e. rfq_panel . as_deref ( ) )
566- == Some ( RFQ_PANEL_GATE )
595+ . and_then ( |e| e. rfq_default . as_deref ( ) )
596+ == Some ( RFQ_DEFAULT_GATE )
567597 }
568598
569599 fn validate ( & self ) -> anyhow:: Result < ( ) > {
@@ -696,6 +726,34 @@ impl Config {
696726 }
697727}
698728
729+ /// True when `toml` sets `[experimental] rfq_default` to the exact gate token.
730+ /// Used for a fleet `panel.toml` that is not a full bot config.
731+ pub fn toml_has_rfq_default_gate ( toml : & str ) -> bool {
732+ let Ok ( value) = toml:: from_str :: < toml:: Value > ( toml) else {
733+ return false ;
734+ } ;
735+ value
736+ . get ( "experimental" )
737+ . and_then ( |e| e. get ( "rfq_default" ) )
738+ . and_then ( |v| v. as_str ( ) )
739+ == Some ( RFQ_DEFAULT_GATE )
740+ }
741+
742+ /// Fleet-wide RFQ-default switch: `{dir}/panel.toml` with the exact token.
743+ pub fn rfq_default_flag_in_dir ( dir : & std:: path:: Path ) -> bool {
744+ let path = dir. join ( PANEL_FLAGS_FILE ) ;
745+ std:: fs:: read_to_string ( path)
746+ . ok ( )
747+ . is_some_and ( |text| toml_has_rfq_default_gate ( & text) )
748+ }
749+
750+ /// Stamp a fresh corridor template as RFQ-only: fleet flag, or this bot
751+ /// already left the public ladder / carries the per-bot token.
752+ pub fn rfq_default_preset_applies ( cfg : Option < & Config > , bots_dir : & std:: path:: Path ) -> bool {
753+ rfq_default_flag_in_dir ( bots_dir)
754+ || cfg. is_some_and ( |c| !c. book_enabled || c. rfq_default_unlocked ( ) )
755+ }
756+
699757#[ cfg( test) ]
700758mod tests {
701759 use super :: * ;
@@ -992,6 +1050,14 @@ mod tests {
9921050 assert ! ( !cfg. rfq_active( ) ) ;
9931051 assert ! ( !cfg. rfq_panel_unlocked( ) ) ;
9941052 assert ! ( cfg. pools. iter( ) . all( |p| p. rfq_corridor. is_none( ) ) ) ;
1053+ let book = EXAMPLE
1054+ . find ( "# book_enabled = false" )
1055+ . expect ( "example must document book_enabled" ) ;
1056+ let feed = EXAMPLE . find ( "\n [feed]\n " ) . expect ( "example has [feed]" ) ;
1057+ assert ! (
1058+ book < feed,
1059+ "book_enabled must be a root key, before [feed] — after [[pools]] it is ignored"
1060+ ) ;
9951061 }
9961062
9971063 #[ test]
@@ -1011,6 +1077,75 @@ mod tests {
10111077 assert ! ( Config :: from_toml( & toml) . unwrap( ) . rfq_panel_unlocked( ) ) ;
10121078 }
10131079
1080+ #[ test]
1081+ fn book_enabled_defaults_on_and_can_be_turned_off ( ) {
1082+ let cfg = Config :: from_toml ( LEAN_POOL_BASE ) . unwrap ( ) ;
1083+ assert ! ( cfg. book_enabled, "omitted book_enabled must stay on" ) ;
1084+
1085+ // Root-level key — appending after [[pools]] would land on the pool.
1086+ let off = LEAN_POOL_BASE . replace (
1087+ "tick_interval_secs = 5" ,
1088+ "tick_interval_secs = 5\n book_enabled = false" ,
1089+ ) ;
1090+ assert ! ( !Config :: from_toml( & off) . unwrap( ) . book_enabled) ;
1091+ }
1092+
1093+ #[ test]
1094+ fn rfq_default_gate_only_opens_on_the_exact_token ( ) {
1095+ let cfg = Config :: from_toml ( LEAN_POOL_BASE ) . unwrap ( ) ;
1096+ assert ! ( !cfg. rfq_default_unlocked( ) ) ;
1097+ assert ! ( !cfg. rfq_panel_unlocked( ) ) ;
1098+
1099+ for wrong in [ "true" , "1" , "enable-rfq-beta" , "ENABLE-RFQ" ] {
1100+ let toml = format ! ( "{LEAN_POOL_BASE}\n [experimental]\n rfq_default = \" {wrong}\" \n " ) ;
1101+ let cfg = Config :: from_toml ( & toml) . unwrap ( ) ;
1102+ assert ! ( !cfg. rfq_default_unlocked( ) , "{wrong:?} must not unlock" ) ;
1103+ }
1104+
1105+ let toml = format ! ( "{LEAN_POOL_BASE}\n [experimental]\n rfq_default = \" enable-rfq\" \n " ) ;
1106+ let cfg = Config :: from_toml ( & toml) . unwrap ( ) ;
1107+ assert ! ( cfg. rfq_default_unlocked( ) ) ;
1108+ assert ! (
1109+ cfg. rfq_panel_unlocked( ) ,
1110+ "the default gate also unlocks the RFQ card"
1111+ ) ;
1112+ }
1113+
1114+ #[ test]
1115+ fn a_panel_toml_is_not_a_bot_config_but_still_trips_the_gate ( ) {
1116+ assert ! ( !toml_has_rfq_default_gate( "" ) ) ;
1117+ assert ! ( !toml_has_rfq_default_gate(
1118+ "[experimental]\n rfq_panel = \" enable-rfq-beta\" \n "
1119+ ) ) ;
1120+ assert ! ( toml_has_rfq_default_gate(
1121+ "[experimental]\n rfq_default = \" enable-rfq\" \n "
1122+ ) ) ;
1123+ }
1124+
1125+ #[ test]
1126+ fn rfq_default_preset_applies_for_fleet_or_an_already_rfq_only_bot ( ) {
1127+ let empty = std:: env:: temp_dir ( ) . join ( "stitch-rfq-preset-applies-empty" ) ;
1128+ let _ = std:: fs:: create_dir_all ( & empty) ;
1129+ let book = Config :: from_toml ( LEAN_POOL_BASE ) . unwrap ( ) ;
1130+ assert ! ( !rfq_default_preset_applies( Some ( & book) , & empty) ) ;
1131+
1132+ let off = LEAN_POOL_BASE . replace (
1133+ "tick_interval_secs = 5" ,
1134+ "tick_interval_secs = 5\n book_enabled = false" ,
1135+ ) ;
1136+ let rfq_only = Config :: from_toml ( & off) . unwrap ( ) ;
1137+ assert ! ( rfq_default_preset_applies( Some ( & rfq_only) , & empty) ) ;
1138+
1139+ let fleet = std:: env:: temp_dir ( ) . join ( "stitch-rfq-preset-applies-fleet" ) ;
1140+ let _ = std:: fs:: create_dir_all ( & fleet) ;
1141+ std:: fs:: write (
1142+ fleet. join ( PANEL_FLAGS_FILE ) ,
1143+ "[experimental]\n rfq_default = \" enable-rfq\" \n " ,
1144+ )
1145+ . unwrap ( ) ;
1146+ assert ! ( rfq_default_preset_applies( Some ( & book) , & fleet) ) ;
1147+ }
1148+
10141149 const RFQ_BLOCK : & str = r#"
10151150 [rfq]
10161151 enabled = true
0 commit comments