# perpSnapshot

{% hint style="info" %}
💧**New endpoint - this endpoint is not a part of original Hyperliquid API and is added by us for builder convenience.**
{% endhint %}

{% hint style="warning" %}

### ⚠️ This is an add-on endpoint - access has to be purchased separately.

{% endhint %}

### Overview

perpSnapshot endpoint allows you to access the entire positioning of all traders on Hyperliquid across all assets, including entries and liquidations.

There are two ways to access the data:

1. **Metadata Endpoint (Fast)**: Check for updates without downloading data
2. **Snapshots Endpoint (Heavy)**: Download actual market snapshot data

<details>

<summary>Efficient polling pattern</summary>

To minimize bandwidth and server load, you should:

1. Poll the metadata endpoint to check for updates
2. Only call the snapshots endpoint when data has changed
3. Cache the snapshot ID locally to avoid unnecessary downloads

</details>

<details>

<summary>Dependencies and best practices</summary>

**Dependencies**

**JavaScript**

* **zstd**: For decompression (e.g., `fzstd`, `@gera2ld/zstd`)
* **msgpack**: For decoding (e.g., `@msgpack/msgpack`)

**Python**

* **zstd**: For decompression (e.g., `zstandard`)
* **msgpack**: For decoding (e.g., `msgpack`)
* **aiohttp**: For async HTTP requests

***

✅ **Best Practices**

1. **Always poll metadata first** - Don't skip the lightweight metadata check.
2. **Cache snapshots locally** - Avoid unnecessary downloads of large files.
3. **Handle errors gracefully** - Network issues are common; implement retry logic.
4. **Use appropriate poll intervals** - 5-10 seconds is usually sufficient.
5. **Request specific markets** - Don't use `['ALL']` unless you truly need all data.
6. **Monitor bandwidth usage** - Snapshots can be large (1-13MB+).

</details>

<details>

<summary>Error handling</summary>

* **404**: No snapshots are available yet. Wait and retry.
* **500**: A server error occurred. Retry with exponential backoff.
* **Network errors**: Implement retry logic with jitter to avoid overwhelming the server.
* **Parse errors**: Log the issue and continue using cached data if available.

</details>

### Perp Snapshot Metadata (Fast)

* **Endpoint**: `POST /info`
* **Purpose**: Check if snapshots have been updated
* **Response Time**: \~1ms

#### Request

```json
{
  "type": "perpSnapshotTimestamp"
}
```

#### Response

```json
{
  "snapshot_id": "20240915_state_123456789",
  "timestamp": 1694764800
}
```

### Perp Snapshots (Heavy)

* **Endpoint**: `POST /info`
* **Purpose**: Download market snapshot data
* **Response Time**: \~100ms+

#### Request

```json
{
  "type": "perpSnapshots",
  "market_names": ["BTC", "FARTCOIN"]  // or ["ALL"] for all core markets
}
```

## Query Options (HIP-3 Multi-Dex)

**Specific markets:**

* `["BTC", "ETH"]` → Defaults to the main dex
* `["xyz:BTC", "vntl:ETH"]` → Specify DEX with `dex:market` format

**All markets patterns:**

* `["ALL"]` → All markets from the main dex
* `["ALL:xyz"]` → All markets from xyz DEX
* `["ALL:ALL_DEXES"]` → All markets from all available DEXes (takes precedence over everything)

**Mixed queries:**

* `["ALL", "ALL:xyz", "BTC"]` → All from main dex + all from xyz (BTC ignored, covered by main dex)
* `["ALL:xyz", "vntl:ETH"]` → All from xyz + specific ETH from vntls

**Rules:**

1. `ALL:ALL_DEXES` takes precedence - if present, returns everything from all dexes
2. Multiple `ALL` patterns allowed - one per dex (e.g., `["ALL", "ALL:xyz"]` gets all from both)
3. Specific markets are filtered - if a dex is covered by an ALL pattern, specific markets from that dex are ignored
4. No duplicates - each dex's ALL pattern is deduplicated

#### Response Headers

* **Single Market**: `x-payload-format: msgpack`, `Content-Encoding: zstd`
* **Multiple Markets**: `x-payload-format: multi-zstd`, `x-compression: inner-zstd`

### Data Format

<details>

<summary>Single Market Response</summary>

**Format**: Raw zstd-compressed MessagePack data

**Decompressed Structure:**

{% code overflow="wrap" %}

```json
[
  "20240915_state_123456789",       // Snapshot ID
  "BTC",                            // Market ID
  [                                 // Positions array
    [1.5, 1500.0, -2.5, 1000.0, 0.0, 5.0, 950.0, 10000.0],
    [/* more position tuples */]
  ],
  [                                 // Addresses array
    "0x1234567890abcdef...",
    "0xfedcba0987654321...",
    /* more addresses */
  ]
}
```

{% endcode %}

ℹ️ **Position Tuple Format** The array `p` contains tuples with the following 8 values in order:

```
size
notional_size
funding_pnl
entry_price
leverage_type_flag // 0 is cross and 1 is isolated
leverage_multiplier
liquidation_price  // Adjusted for unified/PM cross-collateral
account_value      // Reflects unified or PM total account value
```

{% hint style="info" %}
**Unified & Portfolio Margin accounts:** For unified account users, `account_value` includes spot collateral across all dexes. For Portfolio Margin users, `account_value` reflects the per-settlement-token equity and `liquidation_price` is recomputed using PM available margin. Use the `accountValueSnapshots` endpoint to get the full PM breakdown (pm\_ratio, avail per branch).
{% endhint %}

</details>

<details>

<summary>Multiple Markets Response</summary>

**Format**: Custom binary format with length prefixes

**Binary Structure:**

{% code overflow="wrap" %}

```
[market_count: 4 bytes][length1: 4 bytes][zstd_data1][length2: 4 bytes][zstd_data2]...
```

{% endcode %}

ℹ️ **Position Tuple Format** The array `p` contains tuples with the following 8 values in order:

```
size
notional_size
funding_pnl
entry_price
leverage_type_flag // 0 is cross and 1 is isolated
leverage_multiplier
liquidation_price  // Adjusted for unified/PM cross-collateral
account_value      // Reflects unified or PM total account value
```

{% hint style="info" %}
**Unified & Portfolio Margin accounts:** For unified account users, `account_value` includes spot collateral across all dexes. For Portfolio Margin users, `account_value` reflects the per-settlement-token equity and `liquidation_price` is recomputed using PM available margin. Use the `accountValueSnapshots` endpoint to get the full PM breakdown (pm\_ratio, avail per branch).
{% endhint %}

</details>

### Implementation examples

**Note**: Make sure to set **HYDROMANCER\_API\_KEY** in your .env file

Note: Make sure to download required packages

{% tabs %}
{% tab title="JavaScript" %}

```javascript
const axios = require('axios');
const { compress, decompress } = require('@mongodb-js/zstd');
const msgpack = require('msgpack-lite');
const fs = require('fs').promises;
require('dotenv').config();

class PerpSnapshotClient {
    constructor() {
        this.baseUrl = process.env.HYDROMANCER_API_URL || 'https://api.hydromancer.xyz';
        this.apiKey = process.env.HYDROMANCER_API_KEY;
        
        if (!this.apiKey || this.apiKey === 'INSERT_API_KEY_HERE') {
            throw new Error('API key not found. Please set HYDROMANCER_API_KEY in .env file');
        }
        
        this.cachedTimestamp = null;
        this.cachedSnapshots = {};
        this.outputFile = 'perp_snapshot.json';
    }

    async checkForUpdates() {
        const headers = {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${this.apiKey}`
        };

        try {
            const response = await axios.post(
                `${this.baseUrl}/info`,
                { type: 'perpSnapshotTimestamp' },
                { headers }
            );

            const currentTimestamp = response.data.timestamp || response.data.snapshot_id;
            const hasUpdates = this.cachedTimestamp !== currentTimestamp;
            
            if (hasUpdates) {
                console.log(`Perp snapshot updated: ${this.cachedTimestamp} -> ${currentTimestamp}`);
                this.cachedTimestamp = currentTimestamp;
            } else {
                console.log(`No perp snapshot updates (timestamp: ${currentTimestamp})`);
            }

            return hasUpdates;
        } catch (error) {
            throw new Error(`Timestamp request failed: ${error.response?.status} - ${error.response?.data || error.message}`);
        }
    }

    async pollSnapshots(marketNames = ['BTC', 'ETH']) {
        try {
            const hasUpdates = await this.checkForUpdates();
            
            if (!hasUpdates) {
                console.log('No updates available, using cached perp data');
                return this.cachedSnapshots;
            }

            console.log('New perp snapshots available, downloading...');
            const snapshots = await this.downloadSnapshots(marketNames);
            
            this.cachedSnapshots = snapshots;
            await this.saveToFile(snapshots);
            
            return snapshots;
        } catch (error) {
            console.error('Error polling perpetual snapshots:', error);
            throw error;
        }
    }

    async downloadSnapshots(marketNames) {
        const headers = {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${this.apiKey}`
        };

        try {
            const response = await axios.post(
                `${this.baseUrl}/info`,
                { type: 'perpSnapshots', market_names: marketNames },
                { 
                    headers,
                    responseType: 'arraybuffer'
                }
            );

            const payloadFormat = response.headers['x-payload-format'];
            const binaryData = Buffer.from(response.data);
            
            console.log(`Payload format: ${payloadFormat}`);
            console.log(`Downloaded ${binaryData.length} bytes`);
            
            if (payloadFormat === 'multi-zstd') {
                return await this.parseMultipleMarkets(binaryData);
            } else {
                // Single market - could be msgpack or zstd compressed
                return await this.parseSingleMarket(binaryData, marketNames[0]);
            }
        } catch (error) {
            throw new Error(`Perp snapshots request failed: ${error.response?.status} - ${error.response?.data || error.message}`);
        }
    }

    async parseSingleMarket(binaryData, marketName) {
        try {
            // Try to decompress first (if it's zstd compressed)
            let decompressed;
            try {
                decompressed = await decompress(binaryData);
                console.log('Successfully decompressed single market data');
            } catch (e) {
                // If decompression fails, assume it's already uncompressed msgpack
                console.log('Data appears to be uncompressed msgpack');
                decompressed = binaryData;
            }
            
            // Decode msgpack
            const data = msgpack.decode(decompressed);
            
            // According to docs, the structure could be:
            // For perps: [identifier, market, positions, addresses]
            if (Array.isArray(data) && data.length >= 4) {
                const [identifier, market, positions, addresses] = data;
                return {
                    [market]: {
                        identifier: identifier,
                        market: market,
                        positions: positions || [],
                        addresses: addresses || [],
                        timestamp: this.cachedTimestamp
                    }
                };
            }
            
            // Or it could be the object format from docs: {i, m, p, a}
            if (data.i !== undefined && data.m !== undefined) {
                return {
                    [marketName || data.m]: {
                        identifier: data.i,
                        market: data.m,
                        positions: data.p || [],
                        addresses: data.a || [],
                        timestamp: this.cachedTimestamp
                    }
                };
            }
            
            throw new Error('Unknown single market data format');
        } catch (error) {
            console.error('Error parsing single market:', error);
            throw error;
        }
    }

    async parseMultipleMarkets(binaryData) {
        let offset = 0;
        
        // Read number of snapshots (4 bytes, little-endian)
        const count = binaryData.readUInt32LE(offset);
        offset += 4;
        
        console.log(`Number of perp snapshots: ${count}`);
        
        const markets = {};
        
        for (let i = 0; i < count; i++) {
            // Read length of this snapshot (4 bytes, little-endian)
            const length = binaryData.readUInt32LE(offset);
            offset += 4;
            
            console.log(`Snapshot ${i + 1}/${count}: ${length} bytes`);
            
            // Extract and decompress snapshot
            const zstdData = binaryData.slice(offset, offset + length);
            
            try {
                const decompressed = await decompress(zstdData);
                const data = msgpack.decode(decompressed);
                
                // Handle array format: [identifier, market, positions, addresses]
                if (Array.isArray(data) && data.length >= 4) {
                    const [identifier, market, positions, addresses] = data;
                    markets[market] = {
                        identifier: identifier,
                        market: market,
                        positions: positions || [],
                        addresses: addresses || [],
                        timestamp: this.cachedTimestamp
                    };
                    console.log(`Parsed market: ${market} with ${positions?.length || 0} positions`);
                }
                // Handle object format: {i, m, p, a}
                else if (data.i !== undefined && data.m !== undefined) {
                    markets[data.m] = {
                        identifier: data.i,
                        market: data.m,
                        positions: data.p || [],
                        addresses: data.a || [],
                        timestamp: this.cachedTimestamp
                    };
                    console.log(`Parsed market: ${data.m} with ${data.p?.length || 0} positions`);
                }
            } catch (error) {
                console.error(`Error parsing snapshot ${i + 1}:`, error);
            }
            
            offset += length;
        }
        
        return markets;
    }

    async saveToFile(snapshots) {
        const outputData = {
            timestamp: this.cachedTimestamp,
            markets: {}
        };
        
        for (const [market, data] of Object.entries(snapshots)) {
            const positions = data.positions || [];
            const addresses = data.addresses || [];
            
            // Create readable position list (first 100 positions)
            const positionList = [];
            const numPositions = Math.min(100, positions.length, addresses.length);
            
            for (let i = 0; i < numPositions; i++) {
                if (Array.isArray(positions[i]) && positions[i].length >= 8) {
                    positionList.push({
                        address: addresses[i],
                        size: positions[i][0],
                        notional_size: positions[i][1],
                        funding_pnl: positions[i][2],
                        entry_price: positions[i][3],
                        leverage_type_flag: positions[i][4],
                        leverage_multiplier: positions[i][5],
                        liquidation_price: positions[i][6],
                        account_value: positions[i][7]
                    });
                }
            }
            
            outputData.markets[market] = {
                identifier: data.identifier,
                total_positions: positions.length,
                positions: positionList
            };
        }
        
        await fs.writeFile(this.outputFile, JSON.stringify(outputData, null, 2));
        console.log(`Saved perp snapshot to ${this.outputFile}`);
    }
}

// Example usage
async function main() {
    const client = new PerpSnapshotClient();
    
    while (true) {
        try {
            const snapshots = await client.pollSnapshots(['FARTCOIN', 'HYPE']);
            
            for (const [market, data] of Object.entries(snapshots)) {
                console.log(`Market ${market}: ${data.positions?.length || 0} positions`);
            }
            
            console.log('Waiting 5 seconds...');
            await new Promise(resolve => setTimeout(resolve, 5000));
            
        } catch (error) {
            console.error('Polling error:', error.message);
            await new Promise(resolve => setTimeout(resolve, 5000));
        }
    }
}

if (require.main === module) {
    main().catch(console.error);
}

module.exports = PerpSnapshotClient;
```

{% endtab %}

{% tab title="Python" %}

```python
import asyncio
import aiohttp
import struct
from typing import Optional, Dict, List, Any
import time
import json
import zstandard as zstd
import msgpack
import os
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()


class PerpSnapshotClient:
    def __init__(self):
        self.base_url = os.getenv('HYDROMANCER_API_URL', 'https://api.hydromancer.xyz')
        self.api_key = os.getenv('HYDROMANCER_API_KEY')
        
        if not self.api_key or self.api_key == 'INSERT_API_KEY_HERE':
            raise ValueError("API key not found. Please set HYDROMANCER_API_KEY in .env file")
            
        self.cached_timestamp: Optional[str] = None
        self.cached_snapshots: Dict[str, Any] = {}
        self.session: Optional[aiohttp.ClientSession] = None
        self.output_file = "perp_snapshot.json"

    async def __aenter__(self):
        self.session = aiohttp.ClientSession()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self.session:
            await self.session.close()

    async def check_for_updates(self) -> bool:
        """Check if snapshots have been updated since last poll"""
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.api_key}"
        }

        async with self.session.post(
            f'{self.base_url}/info',
            json={'type': 'perpSnapshotTimestamp'},
            headers=headers
        ) as response:
            if not response.ok:
                text = await response.text()
                raise Exception(f'Timestamp request failed: {response.status} - {text}')

            data = await response.json()
            current_timestamp = data.get('timestamp', data.get('snapshot_id'))
            
            has_updates = self.cached_timestamp != current_timestamp
            
            if has_updates:
                print(f'Perp snapshot updated: {self.cached_timestamp} -> {current_timestamp}')
                self.cached_timestamp = current_timestamp
            else:
                print(f'No perp snapshot updates (timestamp: {current_timestamp})')

            return has_updates

    async def poll_snapshots(self, market_names: List[str] = ["BTC", "ETH"]) -> Dict[str, Any]:
        """Efficiently poll for perpetual snapshots"""
        try:
            has_updates = await self.check_for_updates()
            
            if not has_updates:
                print('No updates available, using cached perp data')
                return self.cached_snapshots

            print('New perp snapshots available, downloading...')
            snapshots = await self.download_snapshots(market_names)
            
            self.cached_snapshots = snapshots
            await self.save_to_file(snapshots)
            
            return snapshots
        except Exception as error:
            print(f'Error polling perpetual snapshots: {error}')
            raise

    async def download_snapshots(self, market_names: List[str]) -> Dict[str, Any]:
        """Download perpetual market snapshots"""
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.api_key}"
        }

        async with self.session.post(
            f'{self.base_url}/info',
            json={'type': 'perpSnapshots', 'market_names': market_names},
            headers=headers
        ) as response:
            if not response.ok:
                text = await response.text()
                raise Exception(f'Perp snapshots request failed: {response.status} - {text}')

            payload_format = response.headers.get('x-payload-format')
            binary_data = await response.read()
            
            print(f"Payload format: {payload_format}")
            print(f"Downloaded {len(binary_data)} bytes")
            
            if payload_format == 'multi-zstd':
                return await self.parse_multiple_markets(binary_data)
            else:
                # Single market - could be msgpack or zstd compressed
                return await self.parse_single_market(binary_data, market_names[0])

    async def parse_single_market(self, binary_data: bytes, market_name: str) -> Dict[str, Any]:
        """Parse single market response"""
        try:
            # Try to decompress first (if it's zstd compressed)
            try:
                decompressed = self.decompress_zstd(binary_data)
                print('Successfully decompressed single market data')
            except:
                # If decompression fails, assume it's already uncompressed msgpack
                print('Data appears to be uncompressed msgpack')
                decompressed = binary_data
            
            # Decode msgpack
            data = msgpack.unpackb(decompressed, raw=False)
            
            # For perps: [identifier, market, positions, addresses]
            if isinstance(data, list) and len(data) >= 4:
                identifier, market, positions, addresses = data[:4]
                return {
                    market: {
                        'identifier': identifier,
                        'market': market,
                        'positions': positions or [],
                        'addresses': addresses or [],
                        'timestamp': self.cached_timestamp
                    }
                }
            
            # Or object format: {i, m, p, a}
            if isinstance(data, dict) and 'i' in data and 'm' in data:
                return {
                    market_name or data['m']: {
                        'identifier': data['i'],
                        'market': data['m'],
                        'positions': data.get('p', []),
                        'addresses': data.get('a', []),
                        'timestamp': self.cached_timestamp
                    }
                }
            
            raise ValueError('Unknown single market data format')
        except Exception as error:
            print(f'Error parsing single market: {error}')
            raise

    async def parse_multiple_markets(self, binary_data: bytes) -> Dict[str, Any]:
        """Parse multiple markets response (multi-zstd format)"""
        offset = 0
        
        # Read number of snapshots (4 bytes, little-endian)
        count = struct.unpack('<I', binary_data[offset:offset + 4])[0]
        offset += 4
        
        print(f"Number of perp snapshots: {count}")
        
        markets = {}
        
        for i in range(count):
            # Read length of this snapshot (4 bytes, little-endian)
            length = struct.unpack('<I', binary_data[offset:offset + 4])[0]
            offset += 4
            
            print(f"Snapshot {i + 1}/{count}: {length} bytes")
            
            # Extract and decompress snapshot
            zstd_data = binary_data[offset:offset + length]
            
            try:
                decompressed = self.decompress_zstd(zstd_data)
                data = msgpack.unpackb(decompressed, raw=False)
                
                # Handle array format: [identifier, market, positions, addresses]
                if isinstance(data, list) and len(data) >= 4:
                    identifier, market, positions, addresses = data[:4]
                    markets[market] = {
                        'identifier': identifier,
                        'market': market,
                        'positions': positions or [],
                        'addresses': addresses or [],
                        'timestamp': self.cached_timestamp
                    }
                    print(f"Parsed market: {market} with {len(positions) if positions else 0} positions")
                    
                # Handle object format: {i, m, p, a}
                elif isinstance(data, dict) and 'i' in data and 'm' in data:
                    market = data['m']
                    markets[market] = {
                        'identifier': data['i'],
                        'market': market,
                        'positions': data.get('p', []),
                        'addresses': data.get('a', []),
                        'timestamp': self.cached_timestamp
                    }
                    print(f"Parsed market: {market} with {len(data.get('p', [])) if data.get('p') else 0} positions")
                    
            except Exception as e:
                print(f"Error parsing snapshot {i + 1}: {e}")
                
            offset += length
        
        return markets

    def decompress_zstd(self, data: bytes) -> bytes:
        """Decompress zstd data"""
        dctx = zstd.ZstdDecompressor()
        return dctx.decompress(data)

    async def save_to_file(self, snapshots: Dict[str, Any]):
        """Save snapshots to JSON file"""
        output_data = {
            'timestamp': self.cached_timestamp,
            'markets': {}
        }
        
        for market, data in snapshots.items():
            positions = data.get('positions', [])
            addresses = data.get('addresses', [])
            
            position_list = []
            
            for i in range(len(positions)):
                if isinstance(positions[i], list) and len(positions[i]) >= 8:
                    position_list.append({
                        'address': addresses[i],
                        'size': positions[i][0],
                        'notional_size': positions[i][1],
                        'funding_pnl': positions[i][2],
                        'entry_price': positions[i][3],
                        'leverage_type_flag': positions[i][4],
                        'leverage_multiplier': positions[i][5],
                        'liquidation_price': positions[i][6],
                        'account_value': positions[i][7]
                    })
            
            output_data['markets'][market] = {
                'identifier': data.get('identifier'),
                'total_positions': len(positions),
                'positions': position_list
            }
        
        with open(self.output_file, 'w') as f:
            json.dump(output_data, f, indent=2)
        
        print(f"Saved perp snapshot to {self.output_file}")


async def main():
    """Example usage with different market configurations"""
    async with PerpSnapshotClient() as client:
        while True:
            try:
                snapshots = await client.poll_snapshots(['FARTCOIN', 'HYPE'])
                
                for market, data in snapshots.items():
                    print(f"Market {market}: {len(data.get('positions', []))} positions")
                
                print("Waiting 5 seconds...")
                await asyncio.sleep(5)
                
            except Exception as error:
                print(f'Polling error: {error}')
                await asyncio.sleep(5)


if __name__ == "__main__":
    asyncio.run(main())
```

{% endtab %}
{% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hydromancer.xyz/readme/rest-api/market-data/perpsnapshot.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
