> For the complete documentation index, see [llms.txt](https://docs.hydromancer.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.hydromancer.xyz/readme/rest-api/market-data/perptwapsnapshot.md).

# perpTwapSnapshot

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

### Overview

perpTwapSnapshot endpoint allows you to access all active TWAP (Time-Weighted Average Price) orders on Hyperliquid perpetual markets. TWAPs are algorithmic orders that execute over time, splitting large orders into smaller pieces.

There are two ways to access the data:

1. **Metadata Endpoint (Fast)**: Check for updates without downloading data
2. **Snapshots Endpoint (Heavy)**: Download actual TWAP 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

***

</details>

<details>

<summary>Error handling</summary>

* **404**: No TWAP 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 TWAP Snapshot Metadata (Fast)

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

#### Request

```json
{
  "type": "perpTwapSnapshotTimestamp"
}
```

#### Response

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

### Perp TWAP Snapshots (Heavy)

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

#### Request

```json
{
  "type": "perpTwapSnapshots",
  "market_names": ["BTC", "ETH"]
}
```

## 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 TWAP markets from the main dex
* `["ALL:xyz"]` -> All TWAP markets from xyz DEX
* `["ALL:ALL_DEXES"]` -> All TWAP markets from all available DEXes

**Mixed queries:**

* `["ALL", "ALL:xyz", "BTC"]` -> All from main dex + all from xyz
* `["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
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",
  "BTC",
  [
    ["0x1234567890abcdef...", 1, "BTC", true, 1.5, 0.75, 0.75, 75000.0, 50.0, 86400.0, 1694764800000, false, true, "2024-09-15T12:00:00Z", 100],
    ["0xfedcba0987654321...", 2, "BTC", false, 2.0, 1.0, 1.0, 100000.0, 50.0, 43200.0, 1694764800000, true, false, "2024-09-15T13:00:00Z", 50]
  ]
]
```

{% endcode %}

**Snapshot Array Format:**

```
[snapshot_id, market_name, twaps_array]
```

| Index | Field        | Description                                                 |
| ----- | ------------ | ----------------------------------------------------------- |
| 0     | snapshot\_id | Snapshot identifier                                         |
| 1     | market\_name | Market name ("BTC" for main dex, "xyz:BTC" for HIP-3 dexes) |
| 2     | twaps        | Array of TWAP tuples                                        |

**TWAP Tuple Format:**

```
[address, twap_id, asset, is_buy, total_sz, executed_sz, remaining_sz, executed_ntl, progress_pct, duration_secs, start_time_ms, reduce_only, randomize, next_slice_time, slice_number]
```

| Index | Field             | Description                                        |
| ----- | ----------------- | -------------------------------------------------- |
| 0     | address           | User address (hex string)                          |
| 1     | twap\_id          | TWAP ID (unique per user)                          |
| 2     | asset             | Asset symbol (e.g., "BTC", "ETH")                  |
| 3     | is\_buy           | Direction: true = buy, false = sell                |
| 4     | total\_sz         | Total size to execute (normalized)                 |
| 5     | executed\_sz      | Size already executed (normalized)                 |
| 6     | remaining\_sz     | Remaining size (total\_sz - executed\_sz)          |
| 7     | executed\_ntl     | Notional value executed (USD)                      |
| 8     | progress\_pct     | Progress percentage (0-100)                        |
| 9     | duration\_secs    | Total TWAP duration in seconds                     |
| 10    | start\_time\_ms   | TWAP creation timestamp (milliseconds since epoch) |
| 11    | reduce\_only      | Whether TWAP is reduce-only (boolean)              |
| 12    | randomize         | Whether execution timing is randomized (boolean)   |
| 13    | next\_slice\_time | Next slice execution time (ISO timestamp string)   |
| 14    | slice\_number     | Current slice number in execution                  |

</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 %}

Each `zstd_data` blob, when decompressed, contains a single market's TWAP snapshot in the same array format as the single market response.

</details>

### Implementation Examples

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

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

```python
import struct
import aiohttp
import asyncio
import zstandard as zstd
import msgpack
import os
from datetime import datetime

def parse_twap_snapshot(data: list) -> dict:
    """Parse a single TWAP snapshot from decompressed msgpack data."""
    # Format: [snapshot_id, market_name, twaps_array]
    # TWAP tuple: [address, twap_id, asset, is_buy, total_sz, executed_sz, remaining_sz, executed_ntl, progress_pct, duration_secs, start_time_ms, reduce_only, randomize, next_slice_time, slice_number]
    snapshot_id, market_name, twaps_raw = data[0], data[1], data[2]

    twaps = []
    for t in twaps_raw:
        twaps.append({
            "address": t[0],
            "twap_id": t[1],
            "asset": t[2],
            "is_buy": t[3],
            "total_sz": t[4],
            "executed_sz": t[5],
            "remaining_sz": t[6],
            "executed_ntl": t[7],
            "progress_pct": t[8],
            "duration_secs": t[9],
            "start_time_ms": t[10],
            "reduce_only": t[11],
            "randomize": t[12],
            "next_slice_time": t[13],
            "slice_number": t[14]
        })

    return {
        "snapshot_id": snapshot_id,
        "market": market_name,
        "twaps": twaps
    }

def parse_multi_zstd_response(data: bytes) -> list:
    """Parse multi-zstd response format."""
    offset = 0
    count = struct.unpack_from('<I', data, offset)[0]
    offset += 4

    dctx = zstd.ZstdDecompressor()
    markets = []

    for _ in range(count):
        blob_len = struct.unpack_from('<I', data, offset)[0]
        offset += 4
        blob = data[offset:offset + blob_len]
        offset += blob_len

        decompressed = dctx.decompress(blob, max_output_size=blob_len * 20)
        snapshot_data = msgpack.unpackb(decompressed, raw=False)
        markets.append(parse_twap_snapshot(snapshot_data))

    return markets

async def fetch_perp_twap_snapshots(market_names=None):
    """Fetch perp TWAP snapshots from API."""
    api_key = os.environ.get('HYDROMANCER_API_KEY')
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {api_key}'
    }

    request = {'type': 'perpTwapSnapshots'}
    if market_names:
        request['market_names'] = market_names
    else:
        request['market_names'] = ['ALL']

    async with aiohttp.ClientSession() as session:
        async with session.post(
            'https://api.hydromancer.xyz/info',
            json=request,
            headers=headers
        ) as resp:
            payload_format = resp.headers.get('x-payload-format')
            data = await resp.read()

            if payload_format == 'multi-zstd':
                return parse_multi_zstd_response(data)
            else:
                # Single market response - aiohttp auto-decompresses based on Content-Encoding
                snapshot_data = msgpack.unpackb(data, raw=False)
                return [parse_twap_snapshot(snapshot_data)]

async def fetch_perp_twap_timestamp():
    """Fetch perp TWAP snapshot metadata."""
    api_key = os.environ.get('HYDROMANCER_API_KEY')
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {api_key}'
    }

    async with aiohttp.ClientSession() as session:
        async with session.post(
            'https://api.hydromancer.xyz/info',
            json={'type': 'perpTwapSnapshotTimestamp'},
            headers=headers
        ) as resp:
            return await resp.json()

if __name__ == "__main__":
    async def main():
        # Check timestamp first
        metadata = await fetch_perp_twap_timestamp()
        print(f"Snapshot ID: {metadata.get('snapshot_id')}")
        print(f"Timestamp: {metadata.get('timestamp')}")

        # Fetch all TWAP snapshots
        snapshots = await fetch_perp_twap_snapshots(['ALL:ALL_DEXES'])

        for market in snapshots:
            print(f"\nMarket: {market['market']}")
            for twap in market['twaps'][:3]:  # Show first 3 TWAPs
                direction = "BUY" if twap['is_buy'] else "SELL"
                duration_hrs = twap['duration_secs'] / 3600
                start_time = datetime.fromtimestamp(twap['start_time_ms'] / 1000).isoformat()
                print(f"  {twap['address'][:10]}... | {direction} {twap['asset']} | "
                      f"Progress: {twap['progress_pct']:.1f}% (slice {twap['slice_number']}) | "
                      f"Remaining: {twap['remaining_sz']:.4f} | "
                      f"Duration: {duration_hrs:.1f}h | Started: {start_time}")

    asyncio.run(main())
```

{% endtab %}

{% tab title="JavaScript" %}

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

function parseTwapSnapshot(data) {
    // Format: [snapshot_id, market_name, twaps_array]
    // TWAP tuple: [address, twap_id, asset, is_buy, total_sz, executed_sz, remaining_sz, executed_ntl, progress_pct, duration_secs, start_time_ms, reduce_only, randomize, next_slice_time, slice_number]
    const [snapshotId, marketName, twapsRaw] = data;

    const twaps = twapsRaw.map(t => ({
        address: t[0],
        twap_id: t[1],
        asset: t[2],
        is_buy: t[3],
        total_sz: t[4],
        executed_sz: t[5],
        remaining_sz: t[6],
        executed_ntl: t[7],
        progress_pct: t[8],
        duration_secs: t[9],
        start_time_ms: t[10],
        reduce_only: t[11],
        randomize: t[12],
        next_slice_time: t[13],
        slice_number: t[14]
    }));

    return {
        snapshot_id: snapshotId,
        market: marketName,
        twaps: twaps
    };
}

async function parseMultiZstdResponse(buffer) {
    let offset = 0;
    const count = buffer.readUInt32LE(offset);
    offset += 4;

    const markets = [];

    for (let i = 0; i < count; i++) {
        const blobLen = buffer.readUInt32LE(offset);
        offset += 4;
        const blob = buffer.slice(offset, offset + blobLen);
        offset += blobLen;

        const decompressed = await decompress(blob);
        const snapshotData = msgpack.decode(decompressed);
        markets.push(parseTwapSnapshot(snapshotData));
    }

    return markets;
}

async function fetchPerpTwapSnapshots(marketNames = ['ALL']) {
    const apiKey = process.env.HYDROMANCER_API_KEY;
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`
    };

    const response = await axios.post(
        'https://api.hydromancer.xyz/info',
        { type: 'perpTwapSnapshots', market_names: marketNames },
        { headers, responseType: 'arraybuffer' }
    );

    const payloadFormat = response.headers['x-payload-format'];
    const buffer = Buffer.from(response.data);

    if (payloadFormat === 'multi-zstd') {
        return await parseMultiZstdResponse(buffer);
    } else {
        // Single market response - HTTP client auto-decompresses based on Content-Encoding
        const snapshotData = msgpack.decode(buffer);
        return [parseTwapSnapshot(snapshotData)];
    }
}

async function fetchPerpTwapTimestamp() {
    const apiKey = process.env.HYDROMANCER_API_KEY;
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`
    };

    const response = await axios.post(
        'https://api.hydromancer.xyz/info',
        { type: 'perpTwapSnapshotTimestamp' },
        { headers }
    );

    return response.data;
}

async function main() {
    // Check timestamp first
    const metadata = await fetchPerpTwapTimestamp();
    console.log(`Snapshot ID: ${metadata.snapshot_id}`);
    console.log(`Timestamp: ${metadata.timestamp}`);

    // Fetch all TWAP snapshots
    const snapshots = await fetchPerpTwapSnapshots(['ALL:ALL_DEXES']);

    for (const market of snapshots) {
        console.log(`\nMarket: ${market.market}`);
        for (const twap of market.twaps.slice(0, 3)) {  // Show first 3 TWAPs
            const direction = twap.is_buy ? 'BUY' : 'SELL';
            const durationHrs = (twap.duration_secs / 3600).toFixed(1);
            const startTime = new Date(twap.start_time_ms).toISOString();
            console.log(`  ${twap.address.slice(0, 10)}... | ${direction} ${twap.asset} | ` +
                `Progress: ${twap.progress_pct.toFixed(1)}% (slice ${twap.slice_number}) | ` +
                `Remaining: ${twap.remaining_sz.toFixed(4)} | ` +
                `Duration: ${durationHrs}h | Started: ${startTime}`);
        }
    }
}

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

module.exports = { fetchPerpTwapSnapshots, fetchPerpTwapTimestamp };
```

{% endtab %}
{% endtabs %}
