nautechsystems/nautilus_trader · error
Lighter {label} subscriptions require a perpetual instrument
Error message
Lighter {label} subscriptions require a perpetual instrument: {instrument_id} What it means
perp_market_stats_channel resolves an instrument to a Lighter MarketStats websocket channel, but only CryptoPerpetual instruments are eligible. The instrument was found in the local cache yet is not a perpetual (e.g. a spot or other instrument type), so the subscription is refused.
Source
Thrown at crates/adapters/lighter/src/data/mod.rs:789
self.spawn_task(async move {
if let Err(e) = unsubscribe_market_stats_channel(ws, channel).await {
log::error!("Failed to unsubscribe from Lighter {label}: {e:?}");
}
});
}
}
fn perp_market_stats_channel(
&self,
instrument_id: InstrumentId,
label: &str,
) -> anyhow::Result<LighterWsChannel> {
let instrument = self
.instruments
.get_cloned(&instrument_id)
.ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
anyhow::ensure!(
matches!(instrument, InstrumentAny::CryptoPerpetual(_)),
"Lighter {label} subscriptions require a perpetual instrument: {instrument_id}",
);
let market_index = self.registry.market_index(&instrument_id).ok_or_else(|| {
anyhow::anyhow!("No Lighter market_index registered for {instrument_id}")
})?;
Ok(LighterWsChannel::MarketStats(
LighterMarketSelection::Market(market_index),
))
}
fn index_market_stats_channel(
&self,
instrument_id: InstrumentId,
) -> anyhow::Result<LighterWsChannel> {
let instrument = selfView on GitHub (pinned to 18893faf8b)
Solutions
- Use a perpetual instrument ID (e.g. the PERP symbol) for mark-price and funding-rate subscriptions.
- Filter your subscription list to perpetual instruments before calling subscribe_mark_prices/subscribe_funding_rates.
- Check the instrument cache to confirm what type the ID resolves to.
Example fix
// before
data.subscribe_funding_rates(InstrumentId::from("ETH/USDT.LIGHTER"))?; // spot
// after
data.subscribe_funding_rates(InstrumentId::from("ETH-PERP.USDT.LIGHTER"))?; Defensive patterns
Strategy: validation
Validate before calling
if !instrument_id.symbol.value.ends_with("-PERP") {
return Err(anyhow::anyhow!("mark price/funding requires a perp: {instrument_id}"));
} Type guard
fn is_perp(inst: &InstrumentAny) -> bool { matches!(inst, InstrumentAny::CryptoPerpetual(_)) } Try / catch
match subscribe_funding_rates(instrument_id) {
Ok(()) => (),
Err(e) if e.to_string().contains("require a perpetual") => warn!("skip non-perp {instrument_id}"),
Err(e) => return Err(e),
} Prevention
- Filter subscription lists to CryptoPerpetual instruments before requesting mark prices or funding rates
- Do not request funding rates for spot pairs — they only exist for perps
- Check instrument type in the cache before building subscription requests
When it happens
Trigger: Calling subscribe_mark_prices or subscribe_funding_rates with an InstrumentId that resolves to a non-perpetual cached instrument — e.g. requesting mark prices or funding rates for a spot pair.
Common situations: Subscribing to funding rates for spot instruments (which have none); a stale or wrong instrument ID pointing at a spot symbol; configuring subscriptions from a symbol list that mixes spot and perp symbols.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- WS user data subscription timed out
- subscription confirmation failed: {e}
- invalid Derive subscription channel `{channel}`
- Failed to send UpdateInstrument command: {e}
- Lighter index price subscriptions require a perpetual or spo
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/76b85a1b6d06860c.
Report an issue: GitHub.