Skip to content

testnet API returns invalid token indices causing IndexError #275

Open

Description

@Kaldora-party

Bug Description

The testnet API (api.hyperliquid-testnet.xyz) returns spotMeta data with invalid token indices that cause an IndexError when initializing the Info class.

Error

IndexError: list index out of range

At hyperliquid/info.py:48:

base_info = spot_meta["tokens"][base]  # IndexError when base >= len(tokens)

Root Cause

The testnet spotMeta endpoint returns 1575 tokens but 19 universe entries have token indices >= 1575 (e.g., base: 1576, 1577, 1583).

Evidence

# API response analysis
tokens = 1575 elements (indices 0-1574)
bad_entries = [
  {'name': '@1441', 'base': 1576, 'quote': 0},
  {'name': '@1453', 'base': 1577, 'quote': 0},
  {'name': '@1455', 'base': 1583, 'quote': 0},
  # ... 16 more
]

Suggested Fix

Add bounds checking in info.py before accessing token indices:

tokens = spot_meta["tokens"]
tokens_len = len(tokens)
for spot_info in spot_meta["universe"]:
    base, quote = spot_info["tokens"]
    if base >= tokens_len or quote >= tokens_len:
        continue  # Skip invalid entries
    # ... rest of code

Workaround

I've implemented a local workaround that skips invalid entries, but a proper fix in the SDK would help all users.

Environment

  • SDK version: latest (from pypi)
  • Python 3.10
  • Testnet only (mainnet works fine)

Activity

  1. pat-mok commented on Mar 8, 2026

    @pat-mok

    When will the SDK be updated to fix this issue? I am launching the bot test in vps. So a local fix is not suitable for me.

  2. XYTYX commented on Mar 9, 2026

    @XYTYX

    Getting the same error. In reality the issue is whoever messed it up not importing the correct tokens to testnet.

  3. bug-or-feature commented on Mar 9, 2026

    @bug-or-feature

    @KarolakTibia thanks for flagging this. What is your local workaround?

  4. Kaldora-party commented on Mar 9, 2026

    @Kaldora-party
    Author

    Here's our local workaround - we forked the hyperliquid/info.py module and added defensive error handling:

    # PATCH: Handle testnet API gracefully - spot_meta may be malformed
    if spot_meta is None:
        try:
            spot_meta = self.spot_meta()
        except Exception as e:
            # Testnet API sometimes returns malformed data - skip spot initialization
            spot_meta = None
    
    if spot_meta is not None:
        try:
            for spot_info in spot_meta.get("universe", []):
                asset = spot_info["index"] + 10000
                self.coin_to_asset[spot_info["name"]] = asset
                self.name_to_coin[spot_info["name"]] = spot_info["name"]
                base, quote = spot_info["tokens"]
                # PATCH: Safe access to tokens array  
                if "tokens" in spot_meta and base < len(spot_meta["tokens"]) and quote < len(spot_meta["tokens"]):
                    base_info = spot_meta["tokens"][base]
                    quote_info = spot_meta["tokens"][quote]
                    self.asset_to_sz_decimals[asset] = base_info["szDecimals"]
                    name = f'{base_info["name"]}/{quote_info["name"]}'
                    if name not in self.name_to_coin:
                        self.name_to_coin[name] = spot_info["name"]
        except (IndexError, KeyError, TypeError) as e:
            # Testnet API bug - skip spot initialization, perp trading still works
            pass

    Key differences from the PR #277 fix:

    1. We wrap the entire spot_meta() fetch in a try-except to handle malformed responses
    2. We check bounds before accessing spot_meta["tokens"][base] and [quote]
    3. We catch IndexError, KeyError, TypeError and gracefully skip spot initialization
    4. Perp trading continues to work even when spot data is broken

    This has been running in production on our testnet deployment for several days without issues.

  5. bug-or-feature commented on Mar 9, 2026

    @bug-or-feature

    Here's our local workaround - we forked the hyperliquid/info.py module and added defensive error handling:

    thank you

  6. vagetablechicken commented on Mar 11, 2026

    @vagetablechicken

    Here's our local workaround - we forked the hyperliquid/info.py module and added defensive error handling:

    PATCH: Handle testnet API gracefully - spot_meta may be malformed

    if spot_meta is None:
    try:
    spot_meta = self.spot_meta()
    except Exception as e:
    # Testnet API sometimes returns malformed data - skip spot initialization
    spot_meta = None

    if spot_meta is not None:
    try:
    for spot_info in spot_meta.get("universe", []):
    asset = spot_info["index"] + 10000
    self.coin_to_asset[spot_info["name"]] = asset
    self.name_to_coin[spot_info["name"]] = spot_info["name"]
    base, quote = spot_info["tokens"]
    # PATCH: Safe access to tokens array
    if "tokens" in spot_meta and base < len(spot_meta["tokens"]) and quote < len(spot_meta["tokens"]):
    base_info = spot_meta["tokens"][base]
    quote_info = spot_meta["tokens"][quote]
    self.asset_to_sz_decimals[asset] = base_info["szDecimals"]
    name = f'{base_info["name"]}/{quote_info["name"]}'
    if name not in self.name_to_coin:
    self.name_to_coin[name] = spot_info["name"]
    except (IndexError, KeyError, TypeError) as e:
    # Testnet API bug - skip spot initialization, perp trading still works
    pass
    Key differences from the PR #277 fix:

    1. We wrap the entire spot_meta() fetch in a try-except to handle malformed responses
    2. We check bounds before accessing spot_meta["tokens"][base] and [quote]
    3. We catch IndexError, KeyError, TypeError and gracefully skip spot initialization
    4. Perp trading continues to work even when spot data is broken

    This has been running in production on our testnet deployment for several days without issues.

    @KarolakTibia I see how the workaround differs from the PR. It produces a smaller name_to_coin since certain base/quote pairs fall out of range, whereas we should be leveraging the index from the info field. Of course, this might be negligible for testnet purposes.

    If you have a different perspective on how base_info and quote_info indices/index should be handled, I’d love to hear your thoughts.

  7. Kaldora-party commented on Mar 11, 2026

    @Kaldora-party
    Author

    @vagetablechicken

    Fix Implemented

    I've implemented the suggested fix from the latest comment and it's working well. Here's the approach:

    The Problem

    The testnet API returns malformed spot_meta where token indices in universe entries exceed the bounds of the tokens array, causing IndexError.

    The Solution

    Instead of array position lookup, build an index-based token dictionary:

    # Build index-based token lookup to avoid array position dependency
    token_by_index = {}
    for token in spot_meta.get("tokens", []):
        token_by_index[token["index"]] = token
    
    for spot_info in spot_meta.get("universe", []):
        base_idx, quote_idx = spot_info["tokens"]
        # Use index-based lookup instead of array position
        base_info = token_by_index.get(base_idx)
        quote_info = token_by_index.get(quote_idx)
        if base_info is not None and quote_info is not None:
            self.asset_to_sz_decimals[asset] = base_info["szDecimals"]
            name = f'{base_info["name"]}/{quote_info["name"]}'
            # ... proceed with token info

    Why This Works

    • Each token has an index field that's stable regardless of array position
    • The tokens array in universe entries contains these index values, not array positions
    • By looking up tokens by their index field, we avoid the array bounds issue entirely
    • Gracefully handles malformed responses where indices don't match array positions

    Testing

    Verified with multiple test scenarios including:

    • Normal spot_meta initialization ✓
    • Malformed data with invalid indices (no crash) ✓
    • Index mismatch scenarios (correctly resolves tokens) ✓
    • Empty data and API exceptions ✓

    Thanks for the suggestion to "leverage the index from the info field" - that was the key insight!

  8. mifth commented on Mar 29, 2026

    @mifth

    It seems I made a duplicate:
    #283

  9. marked Crash: basic_adding.py #283 as a duplicate of this issue on Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions