nautechsystems/nautilus_trader · error · anyhow::Error

Outcome side token '{fee_token}' carried a non-zero fee {fee

Error message

Outcome side token '{fee_token}' carried a non-zero fee {fee_amount}; venue does not denominate fees in side tokens

What it means

When resolving the fee currency of a Hyperliquid fill, outcome side tokens (symbols starting with '+') are recognized, but the venue never denominates fees in side tokens. If a fill reports a fee in a side token with a non-zero amount, the parser treats it as an invariant violation and fails rather than misattributing the fee.

Source

Thrown at crates/adapters/hyperliquid/src/http/parse.rs:726

/// the commission currency would leak into `OrderFilled` events and persistence;
/// for outcome side tokens the instrument's quote currency is always used, even
/// when another adapter path (such as spot-balance parsing) has registered the
/// side token in the global registry. Non-zero side-token fees error: the venue
/// does not denominate fees in side tokens. Other unknown tokens fall back to
/// the instrument's quote currency only when the fee is zero.
///
/// # Errors
///
/// Returns an error when an outcome side token carries a non-zero fee, or when
/// `fee_token` cannot be resolved and `fee_amount` is non-zero.
pub fn resolve_fee_currency(
    fee_token: &str,
    fee_amount: Decimal,
    instrument: &dyn Instrument,
) -> anyhow::Result<Currency> {
    if is_outcome_side_token(fee_token) {
        if !fee_amount.is_zero() {
            anyhow::bail!(
                "Outcome side token '{fee_token}' carried a non-zero fee {fee_amount}; \
                 venue does not denominate fees in side tokens",
            );
        }
        return Ok(instrument.quote_currency());
    }

    if let Some(currency) = Currency::try_from_str(fee_token) {
        return Ok(currency);
    }

    if fee_amount.is_zero() {
        let fallback = instrument.quote_currency();
        log::debug!(
            "Unregistered fee token '{fee_token}' on zero-fee fill for {}; using {fallback} as fallback",
            instrument.id(),
        );
        return Ok(fallback);

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check the venue API version/adapter version — update the Hyperliquid adapter if fee reporting for outcomes changed.
  2. Log and report the raw fill payload to verify what token the venue actually charged the fee in.
  3. If the payload is genuine, this is an upstream contract change: file an issue / patch resolve_fee_currency to map the new fee token.
  4. For normal operation, treat this as a parse failure of that fill and continue processing subsequent fills.
Defensive patterns

Strategy: try-catch

Validate before calling

fn fee_token_is_safe(fee_token: &str, fee_amount: Decimal) -> bool {
    !(fee_token.starts_with('+') && !fee_amount.is_zero())
}

Try / catch

match resolve_fee_currency(fee_token, fee_amount, instrument) {
    Ok(cur) => apply_fee(cur, fee_amount),
    Err(e) => log::warn!("skipping fee attribution: {e}"), // side-token fee invariant broken
}

Prevention

When it happens

Trigger: parse_fill_report receiving a fill whose fee token is an outcome side token (leading '+') with fee_amount != 0 — the venue response contains a fee denominated in a side token, which the parser considers impossible.

Common situations: Unexpected venue-side changes to fee denomination on HIP-4 outcome markets; a malformed or spoofed fill payload; running against a venue API version that changed fee reporting for outcomes.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


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