nautechsystems/nautilus_trader · error · anyhow::Error

Binance Spot market data mode SBE requires Ed25519 API…

Error message

Binance Spot market data mode SBE requires Ed25519 API credentials. Set the appropriate env vars for your environment, or provide api_key/api_secret in the data client config

What it means

Binance Spot SBE (Simple Binary Encoding) market data streams require authenticated Ed25519-signed websocket connections. The data client refuses to `connect()` in SBE mode when the underlying websocket client holds no API credentials (`has_credentials()` is false), preventing a doomed connection attempt. Regular JSON market data is public, but SBE requires the key/secret pair.

Solutions

  1. Provide `api_key` and `api_secret` (Ed25519 keypair credentials) in the data client config
  2. Export the Binance Ed25519 API credentials as environment variables for your environment
  3. Verify the credentials are Ed25519 type, not HMAC, as SBE requires Ed25519
  4. If SBE authentication is not needed, switch `spot_market_data_mode` back to the default JSON mode
  5. Check that the process environment actually receives the vars (docker --env-file, k8s secret mounting)

Example fix

// before
DataClientConfig { spot_market_data_mode: Sbe, /* no credentials */ }
// after
DataClientConfig {
    spot_market_data_mode: Sbe,
    api_key: Some(env::var("BINANCE_API_KEY")?),
    api_secret: Some(env::var("BINANCE_API_SECRET")?),
}
Defensive patterns

Strategy: validation

Validate before calling

fn validate_sbe_credentials(config: &DataClientConfig, env: &Env) -> Result<(), String> {
    if config.spot_market_data_mode == BinanceSpotMarketDataMode::Sbe {
        let has_cfg = config.api_key.is_some() && config.api_secret.is_some();
        let has_env = env::var("BINANCE_API_KEY").is_ok() && env::var("BINANCE_API_SECRET").is_ok();
        if !has_cfg && !has_env {
            return Err("SBE mode requires Ed25519 api_key/api_secret (config or env)".into());
        }
    }
    Ok()
}

Type guard

fn has_credentials(c: &DataClientConfig) -> bool {
    c.api_key.as_deref().map_or(false, |k| !k.is_empty())
        && c.api_secret.as_deref().map_or(false, |s| !s.is_empty())
}

Try / catch

match data_client.connect().await {
    Err(e) if e.to_string().contains("requires Ed25519 API credentials") => {
        eprintln!("SBE auth misconfigured: {e:#}");
        // fall back to JSON mode or abort startup
    }
    other => other?,
}

Prevention

When it happens

Trigger: Configuring `spot_market_data_mode = Sbe` on the Binance Spot data client while neither the config provides `api_key`/`api_secret` nor the environment exposes the expected credential env vars, then calling `connect()`.

Common situations: Running locally where env vars (e.g. BINANCE_API_KEY / BINANCE_API_SECRET) are unset because credentials live only in production secrets; switching market data mode to SBE for lower-latency binary feeds without updating the config; container deployments where env vars were not forwarded.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/5feb5b0016916659. Report an issue: GitHub.

Appendix: source

Thrown at crates/adapters/binance/src/spot/data.rs:1493

        Ok(())
    }

    fn dispose(&mut self) -> anyhow::Result<()> {
        log::debug!("Disposing {id}", id = self.client_id);
        self.stop()
    }

    async fn connect(&mut self) -> anyhow::Result<()> {
        if self.is_connected() && self.session_tasks.is_open() && self.command_tasks.is_open() {
            return Ok(());
        }

        register_binance_custom_data();

        if self.spot_market_data_mode == BinanceSpotMarketDataMode::Sbe
            && !self.ws_client.has_credentials()
        {
            anyhow::bail!(
                "Binance Spot market data mode SBE requires Ed25519 API credentials. \
                 Set the appropriate env vars for your environment, \
                 or provide api_key/api_secret in the data client config"
            );
        }

        self.prepare_task_groups().await?;
        let ws_client = self.ws_client.clone();
        let setup_guard =
            TaskGroupGuard::new(&[&self.session_tasks, &self.command_tasks], move || {
                ws_client.begin_shutdown();
            });

        Self::refresh_instrument_catalog(
            &self.http_client,
            &self.config.instrument_provider,
            self.config.us,
            &self.instruments,

View on GitHub (pinned to 18893faf8b)