accountValueSnapshot
Unique endpoint allowing to get an account value snapshot of all accounts on Hyperliquid, including filtering by a specific type of collateral or HIP-3 DEX.
Last updated
{
"type": "accountValueSnapshotTimestamp"
}{
"snapshot_id": "20240915_state_123456789",
"timestamp": 1694764800
}{
"type": "accountValueSnapshots",
"collateral_tokens": ["USDC"]
}// Get all DEXes using USDC as collateral
{"type": "accountValueSnapshots", "collateral_tokens": ["USDC"]}
// Might return: hyperliquid:USDC, xyz:USDC, vntls:USDC (multiple snapshots)
// Get only Hyperliquid's USDC accounts
{"type": "accountValueSnapshots", "collateral_tokens": ["hyperliquid:USDC"]}
// Returns: Only hyperliquid:USDC (single snapshot)
// Get USDC from all DEXes + specific USDE from xyz only
{"type": "accountValueSnapshots", "collateral_tokens": ["USDC", "xyz:USDE"]}
// Returns: All *:USDC snapshots + xyz:USDE snapshot (multiple snapshots)[
"20240915_account_value_123456789", // Snapshot ID
"USDC", // Collateral token
{ // Users map
"0x1234...": [25000.50, 15000.00, 5000.00],
"0x5678...": [100000.00, 50000.00, 30000.00],
// more users...
}
]account_value // Total account value in USD
total_long_notional // Sum of all long position notionals
total_short_notional // Sum of all short position notionals (absolute value)
is_unified // Boolean: true if unified account (spot included in account_value)
account_mode // 0=Standard, 1=DexAbstraction, 2=Unified, 3=PortfolioMarginaccount_value // Per-settlement-token account value (perp equity + spot)
total_long_notional // Sum of all long position notionals
total_short_notional // Sum of all short position notionals (absolute value)
is_unified // Boolean (false for PM users)
account_mode // 3 (PortfolioMargin)
pm_ratio // Portfolio margin ratio (max across all branches)
pm_avail_by_branch // Array of [token_name, available_margin] tuples
pm_total_account_value // Total account value across all collateral tokens"0x1234...": [2597.64, 1500.00, 800.00, false, 3, 0.138, [["USDC", 1999.40], ["USDH", 1000.00]], 5788.74]{
"i": "20240915_account_value_123456789",
"t": "USDC",
"u": {
"0x1234...": {
"v": 25000.50,
"l": 15000.00,
"s": 5000.00,
"m": false,
"am": 0
},
"0x5678...": {
"v": 2597.64,
"l": 1500.00,
"s": 800.00,
"m": false,
"am": 3,
"pm": 0.138,
"pa": [["USDC", 1999.40], ["USDH", 1000.00]],
"pt": 5788.74
}
}
}[token_count: 4 bytes][length1: 4 bytes][zstd_data1][length2: 4 bytes][zstd_data2]...const axios = require('axios');
const { decompress } = require('@mongodb-js/zstd');
const msgpack = require('msgpack-lite');
const fs = require('fs').promises;
require('dotenv').config();
class AccountValueSnapshotClient {
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.cachedSnapshots = {};
this.cachedTimestamp = null;
this.outputFile = 'account_value_snapshot.json';
}
async checkForUpdates() {
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
};
try {
const response = await axios.post(
`${this.baseUrl}/info`,
{ type: 'accountValueSnapshotTimestamp' },
{ headers }
);
const currentTimestamp = response.data.timestamp || response.data.snapshot_id;
const hasUpdates = this.cachedTimestamp !== currentTimestamp;
if (hasUpdates) {
console.log(`Account value snapshot updated: ${this.cachedTimestamp} -> ${currentTimestamp}`);
this.cachedTimestamp = currentTimestamp;
} else {
console.log(`No account value snapshot updates (timestamp: ${currentTimestamp})`);
}
return hasUpdates;
} catch (error) {
throw new Error(`Timestamp request failed: ${error.response?.status} - ${error.response?.data || error.message}`);
}
}
async pollSnapshots(collateralTokens = ['USDC']) {
try {
const hasUpdates = await this.checkForUpdates();
if (!hasUpdates) {
console.log('No updates available, using cached data');
return this.cachedSnapshots;
}
console.log('New snapshots available, downloading...');
const snapshots = await this.downloadSnapshots(collateralTokens);
this.cachedSnapshots = snapshots;
await this.saveToFile(snapshots);
return snapshots;
} catch (error) {
console.error('Error polling account value snapshots:', error.message);
throw error;
}
}
async downloadSnapshots(collateralTokens) {
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
};
try {
const response = await axios.post(
`${this.baseUrl}/info`,
{
type: 'accountValueSnapshots',
collateral_tokens: collateralTokens
},
{
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.parseMultipleTokens(binaryData);
} else {
return await this.parseSingleToken(binaryData, collateralTokens[0]);
}
} catch (error) {
if (error.response) {
throw new Error(`Request failed: ${error.response.status} - ${error.response.statusText}`);
}
throw error;
}
}
async parseSingleToken(binaryData, tokenName) {
try {
// Decompress zstd data
let decompressed;
try {
decompressed = await decompress(binaryData);
console.log('Successfully decompressed single token data');
} catch (e) {
// If decompression fails, assume it's already uncompressed
decompressed = binaryData;
}
// Decode msgpack
const data = msgpack.decode(decompressed);
// Array format: [snapshot_id, token, users_map]
if (Array.isArray(data) && data.length >= 3) {
const [snapshotId, token, usersMap] = data;
return {
[token]: {
snapshot_id: snapshotId,
token: token,
users: this.parseUsers(usersMap),
total_users: Object.keys(usersMap).length
}
};
}
// Object format: {i: snapshot_id, t: token, u: users}
if (data.i !== undefined && data.t !== undefined && data.u !== undefined) {
return {
[data.t]: {
snapshot_id: data.i,
token: data.t,
users: this.parseUsers(data.u),
total_users: Object.keys(data.u).length
}
};
}
throw new Error('Unknown single token data format');
} catch (error) {
console.error('Error parsing single token:', error);
throw error;
}
}
async parseMultipleTokens(binaryData) {
let offset = 0;
// Read number of snapshots (4 bytes, little-endian)
const count = binaryData.readUInt32LE(offset);
offset += 4;
console.log(`Number of account value snapshots: ${count}`);
const tokens = {};
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);
// Array format: [snapshot_id, token, users_map]
if (Array.isArray(data) && data.length >= 3) {
const [snapshotId, token, usersMap] = data;
tokens[token] = {
snapshot_id: snapshotId,
token: token,
users: this.parseUsers(usersMap),
total_users: Object.keys(usersMap).length
};
console.log(`Parsed token: ${token} with ${Object.keys(usersMap).length} users`);
}
// Object format: {i, t, u}
else if (data.i !== undefined && data.t !== undefined && data.u !== undefined) {
tokens[data.t] = {
snapshot_id: data.i,
token: data.t,
users: this.parseUsers(data.u),
total_users: Object.keys(data.u).length
};
console.log(`Parsed token: ${data.t} with ${Object.keys(data.u).length} users`);
}
} catch (error) {
console.error(`Error parsing snapshot ${i + 1}:`, error);
}
offset += length;
}
return tokens;
}
parseUsers(usersMap) {
const userList = [];
for (const [address, userData] of Object.entries(usersMap)) {
// Array format: [account_value, total_long_notional, total_short_notional]
if (Array.isArray(userData) && userData.length >= 3) {
userList.push({
address: address,
account_value: userData[0],
total_long_notional: userData[1],
total_short_notional: userData[2]
});
}
// Object format: {v, l, s}
else if (userData.v !== undefined) {
userList.push({
address: address,
account_value: userData.v,
total_long_notional: userData.l || 0,
total_short_notional: userData.s || 0
});
}
}
// Sort by account value descending
userList.sort((a, b) => b.account_value - a.account_value);
return userList;
}
async saveToFile(snapshots) {
const outputData = {
timestamp: new Date().toISOString(),
tokens: {}
};
for (const [token, data] of Object.entries(snapshots)) {
const users = data.users || [];
// Calculate statistics
const totalAccountValue = users.reduce((sum, u) => sum + u.account_value, 0);
const totalLongNotional = users.reduce((sum, u) => sum + u.total_long_notional, 0);
const totalShortNotional = users.reduce((sum, u) => sum + u.total_short_notional, 0);
outputData.tokens[token] = {
snapshot_id: data.snapshot_id,
total_users: users.length,
statistics: {
total_account_value: totalAccountValue,
total_long_notional: totalLongNotional,
total_short_notional: totalShortNotional,
avg_account_value: users.length > 0 ? totalAccountValue / users.length : 0
},
top_10_users: users.slice(0, 10),
// all_users: users // Uncomment to save all users
};
}
await fs.writeFile(this.outputFile, JSON.stringify(outputData, null, 2));
console.log(`\nSaved account value snapshot to ${this.outputFile}`);
// Print summary
for (const [token, data] of Object.entries(outputData.tokens)) {
const stats = data.statistics;
console.log(`\nToken: ${token}`);
console.log(` Total users: ${data.total_users}`);
console.log(` Total account value: $${stats.total_account_value.toLocaleString()}`);
console.log(` Total long notional: $${stats.total_long_notional.toLocaleString()}`);
console.log(` Total short notional: $${stats.total_short_notional.toLocaleString()}`);
console.log(` Average account value: $${stats.avg_account_value.toLocaleString()}`);
}
}
}
// Example usage - Continuous polling with timestamp check
async function main() {
const client = new AccountValueSnapshotClient();
console.log('Starting continuous polling for USDC account values...');
console.log('Checking for updates every 5 seconds');
console.log('Press Ctrl+C to stop\n');
let pollCount = 0;
while (true) {
try {
pollCount++;
console.log(`\n[${'='.repeat(50)}]`);
console.log(`Poll #${pollCount} at ${new Date().toISOString()}`);
console.log(`[${'='.repeat(50)}]`);
// pollSnapshots automatically checks timestamp first and only downloads if updated
const snapshots = await client.pollSnapshots(['USDC']);
// Log results
for (const [token, data] of Object.entries(snapshots)) {
console.log(`\n${token}:`);
console.log(` Total users: ${data.users?.length || 0}`);
console.log(` Snapshot ID: ${data.snapshot_id}`);
if (data.users && data.users.length > 0) {
const totalValue = data.users.reduce((sum, u) => sum + u.account_value, 0);
console.log(` Total account value: $${totalValue.toLocaleString(undefined, {maximumFractionDigits: 2})}`);
}
}
// Wait 5 seconds before next poll
console.log('\nWaiting 15 seconds before next check...');
await new Promise(resolve => setTimeout(resolve, 15000));
} catch (error) {
console.error('\nPolling error:', error.message);
console.log('Retrying in 5 seconds...');
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
if (require.main === module) {
main().catch(console.error);
}
module.exports = AccountValueSnapshotClient;
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 AccountValueSnapshotClient:
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_snapshots: Dict[str, Any] = {}
self.cached_timestamp: Optional[str] = None
self.session: Optional[aiohttp.ClientSession] = None
self.output_file = "account_value_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': 'accountValueSnapshotTimestamp'},
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'Account value snapshot updated: {self.cached_timestamp} -> {current_timestamp}')
self.cached_timestamp = current_timestamp
else:
print(f'No account value snapshot updates (timestamp: {current_timestamp})')
return has_updates
async def poll_snapshots(self, collateral_tokens: List[str] = ["USDC"]) -> Dict[str, Any]:
"""Efficiently poll for account value snapshots"""
try:
has_updates = await self.check_for_updates()
if not has_updates:
print('No updates available, using cached data')
return self.cached_snapshots
print('New snapshots available, downloading...')
snapshots = await self.download_snapshots(collateral_tokens)
self.cached_snapshots = snapshots
await self.save_to_file(snapshots)
return snapshots
except Exception as error:
print(f'Error polling account value snapshots: {error}')
raise
async def download_snapshots(self, collateral_tokens: List[str]) -> Dict[str, Any]:
"""Download account value snapshots"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
async with self.session.post(
f'{self.base_url}/info',
json={'type': 'accountValueSnapshots', 'collateral_tokens': collateral_tokens},
headers=headers
) as response:
if not response.ok:
text = await response.text()
raise Exception(f'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_tokens(binary_data)
else:
return await self.parse_single_token(binary_data, collateral_tokens[0] if len(collateral_tokens) == 1 else None)
async def parse_single_token(self, binary_data: bytes, token_name: Optional[str] = None) -> Dict[str, Any]:
"""Parse single token response"""
try:
# Decompress zstd data
try:
decompressed = self.decompress_zstd(binary_data)
print('Successfully decompressed single token data')
except:
decompressed = binary_data
# Decode msgpack
data = msgpack.unpackb(decompressed, raw=False)
# Array format: [snapshot_id, token, users_map]
if isinstance(data, list) and len(data) >= 3:
snapshot_id, token, users_map = data[0], data[1], data[2]
return {
token: {
'snapshot_id': snapshot_id,
'token': token,
'users': self.parse_users(users_map),
'total_users': len(users_map)
}
}
# Object format: {i, t, u}
if isinstance(data, dict) and 'i' in data and 't' in data and 'u' in data:
return {
data['t']: {
'snapshot_id': data['i'],
'token': data['t'],
'users': self.parse_users(data['u']),
'total_users': len(data['u'])
}
}
raise ValueError('Unknown single token data format')
except Exception as error:
print(f'Error parsing single token: {error}')
raise
async def parse_multiple_tokens(self, binary_data: bytes) -> Dict[str, Any]:
"""Parse multiple tokens response"""
offset = 0
# Read number of snapshots
count = struct.unpack('<I', binary_data[offset:offset + 4])[0]
offset += 4
print(f"Number of snapshots: {count}")
tokens = {}
for i in range(count):
# Read length
length = struct.unpack('<I', binary_data[offset:offset + 4])[0]
offset += 4
print(f"Snapshot {i + 1}/{count}: {length} bytes")
# Extract and decompress
zstd_data = binary_data[offset:offset + length]
try:
decompressed = self.decompress_zstd(zstd_data)
data = msgpack.unpackb(decompressed, raw=False)
# Array format
if isinstance(data, list) and len(data) >= 3:
snapshot_id, token, users_map = data[0], data[1], data[2]
tokens[token] = {
'snapshot_id': snapshot_id,
'token': token,
'users': self.parse_users(users_map),
'total_users': len(users_map)
}
print(f"Parsed token: {token} with {len(users_map)} users")
# Object format
elif isinstance(data, dict) and 'i' in data and 't' in data and 'u' in data:
token = data['t']
tokens[token] = {
'snapshot_id': data['i'],
'token': token,
'users': self.parse_users(data['u']),
'total_users': len(data['u'])
}
print(f"Parsed token: {token} with {len(data['u'])} users")
except Exception as e:
print(f"Error parsing snapshot {i + 1}: {e}")
offset += length
return tokens
def parse_users(self, users_map: Dict) -> List[Dict[str, Any]]:
"""Parse users data from msgpack format"""
user_list = []
for address, user_data in users_map.items():
# Array format: [account_value, total_long_notional, total_short_notional]
if isinstance(user_data, list) and len(user_data) >= 3:
user_list.append({
'address': address,
'account_value': user_data[0],
'total_long_notional': user_data[1],
'total_short_notional': user_data[2]
})
# Object format: {v, l, s}
elif isinstance(user_data, dict):
user_list.append({
'address': address,
'account_value': user_data.get('v', 0),
'total_long_notional': user_data.get('l', 0),
'total_short_notional': user_data.get('s', 0)
})
# Sort by account value descending
user_list.sort(key=lambda x: x['account_value'], reverse=True)
return user_list
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': time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime()),
'tokens': {}
}
for token, data in snapshots.items():
users = data.get('users', [])
# Calculate statistics
total_account_value = sum(u['account_value'] for u in users)
total_long_notional = sum(u['total_long_notional'] for u in users)
total_short_notional = sum(u['total_short_notional'] for u in users)
output_data['tokens'][token] = {
'snapshot_id': data.get('snapshot_id'),
'total_users': len(users),
'statistics': {
'total_account_value': total_account_value,
'total_long_notional': total_long_notional,
'total_short_notional': total_short_notional,
'avg_account_value': total_account_value / len(users) if users else 0
},
'top_10_users': users[:10],
# 'all_users': users # Uncomment to save all users
}
with open(self.output_file, 'w') as f:
json.dump(output_data, f, indent=2)
print(f"\nSaved account value snapshot to {self.output_file}")
# Print summary
for token, data in output_data['tokens'].items():
stats = data['statistics']
print(f"\nToken: {token}")
print(f" Total users: {data['total_users']}")
print(f" Total account value: ${stats['total_account_value']:,.2f}")
print(f" Total long notional: ${stats['total_long_notional']:,.2f}")
print(f" Total short notional: ${stats['total_short_notional']:,.2f}")
print(f" Average account value: ${stats['avg_account_value']:,.2f}")
async def main():
"""Example usage - Continuous polling with timestamp check"""
async with AccountValueSnapshotClient() as client:
print('Starting continuous polling for USDC account values...')
print('Checking for updates every 5 seconds')
print('Press Ctrl+C to stop\n')
poll_count = 0
while True:
try:
poll_count += 1
print(f"\n[{'=' * 50}]")
print(f"Poll #{poll_count} at {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}")
print(f"[{'=' * 50}]")
# poll_snapshots automatically checks timestamp first and only downloads if updated
snapshots = await client.poll_snapshots(['USDC'])
# Log results
for token, data in snapshots.items():
print(f"\n{token}:")
print(f" Total users: {len(data.get('users', []))}")
print(f" Snapshot ID: {data.get('snapshot_id')}")
users = data.get('users', [])
if users:
total_value = sum(u['account_value'] for u in users)
print(f" Total account value: ${total_value:,.2f}")
# Wait 5 seconds before next poll
print('\nWaiting 15 seconds before next check...')
await asyncio.sleep(15)
except Exception as error:
print(f'\nPolling error: {error}')
print('Retrying in 5 seconds...')
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(main())