Documentation Index
Fetch the complete documentation index at: https://docs.allium.so/llms.txt
Use this file to discover all available pages before exploring further.
Calculating Spot Volume
Calculate total trading volume for spot coins on Hyperliquid using the Allium API. Spot coins are identified as coins whose name starts with@.
Features
- Fetches all fills for a user using the
userFillsByTimeendpoint - Handles pagination (max 2000 results per call)
- Deduplicates fills using (time, coin, tid) as unique key
- Filters for spot coins (starting with
@) - Calculates trading volume as
price * size - Shows progress during execution
Usage
- Go
- Python
- JavaScript
go run main.go --api-key YOUR_API_KEY
# With custom user
go run main.go --api-key YOUR_API_KEY --user 0x...
# Build binary
go build -o spot_volume
./spot_volume --api-key YOUR_API_KEY
uv run spot_volume.py --api-key YOUR_API_KEY
# With custom user
uv run spot_volume.py --api-key YOUR_API_KEY --user 0x...
node spot_volume.js --api-key YOUR_API_KEY
# With custom user
node spot_volume.js --api-key YOUR_API_KEY --user 0x...
# Make executable
chmod +x spot_volume.js
./spot_volume.js --api-key YOUR_API_KEY
Arguments
| Argument | Required | Description |
|---|---|---|
--api-key | Yes | Allium API key |
--user | No | User wallet address (default: 0xc0142fc8aa609f324b44e414816ea549322afbe8) |
Output
The script displays:- Progress for each page fetched
- Number of fills received and processed
- Spot coin fills and volume per page
- Final summary with total statistics
Source Code
- Python
- Go
- JavaScript
#!/usr/bin/env python3
# /// script
# dependencies = [
# "requests",
# ]
# ///
"""
Fetch all fills for a Hyperliquid user and calculate total trading volume for spot coins.
Spot coins are identified by names starting with '@'.
"""
import argparse
import sys
from datetime import datetime
from typing import Set, Tuple
import requests
# API Configuration
API_URL = "https://api.allium.so/api/v1/developer/trading/hyperliquid/info/fills"
DEFAULT_USER = "0xc0142fc8aa609f324b44e414816ea549322afbe8"
# Start from a historical date (e.g., Hyperliquid launch)
START_TIME_MS = 1609459200000 # Jan 1, 2021
def fetch_fills(user: str, start_time: int, api_key: str, end_time: int | None = None) -> list:
"""Fetch fills from the API for a given time range."""
headers = {
"Content-Type": "application/json",
"X-API-KEY": api_key,
}
payload = {
"user": user,
"type": "userFillsByTime",
"startTime": start_time,
}
if end_time:
payload["endTime"] = end_time
print(" Making API request...", flush=True)
try:
response = requests.post(API_URL, json=payload, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching fills: {e}", file=sys.stderr)
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}", file=sys.stderr)
sys.exit(1)
def calculate_spot_volume(user: str, api_key: str) -> None:
"""Calculate total trading volume for spot coins (coins starting with '@')."""
seen_fills: Set[Tuple[int, str, int]] = set() # (time, coin, tid)
total_volume = 0.0
spot_fill_count = 0
total_fill_count = 0
current_start_time = START_TIME_MS
page_count = 0
print(f"Fetching fills for user: {user}", flush=True)
print(f"Starting from: {datetime.fromtimestamp(START_TIME_MS / 1000).isoformat()}", flush=True)
print("-" * 80, flush=True)
while True:
page_count += 1
print(f"\nPage {page_count}: Fetching fills from {datetime.fromtimestamp(current_start_time / 1000).isoformat()}...", flush=True)
fills = fetch_fills(user, current_start_time, api_key)
if not fills:
print("No more fills found.", flush=True)
break
print(f" Received {len(fills)} fills", flush=True)
# Track new fills in this batch
new_fills = 0
new_spot_fills = 0
page_volume = 0.0
for fill in fills:
# Create unique key for this fill
fill_key = (fill.get("time"), fill.get("coin"), fill.get("tid"))
# Skip if we've already seen this fill
if fill_key in seen_fills:
continue
seen_fills.add(fill_key)
new_fills += 1
total_fill_count += 1
coin = fill.get("coin", "")
# Check if it's a spot coin (starts with '@')
if coin.startswith("@"):
sz = fill.get("sz", "0")
px = fill.get("px", "0")
try:
size = float(sz)
price = float(px)
volume = size * price # Trading volume = size * price
total_volume += volume
page_volume += volume
spot_fill_count += 1
new_spot_fills += 1
except (ValueError, TypeError):
print(f" Warning: Could not parse size '{sz}' or price '{px}' for coin {coin}", file=sys.stderr)
print(f" New unique fills: {new_fills} (spot: {new_spot_fills}, volume: {page_volume:.4f})", flush=True)
# Check if we need to continue paging
if len(fills) < 2000:
print("\nReceived less than 2000 fills, pagination complete.", flush=True)
break
if new_fills == 0:
print("\nNo new fills found, all remaining fills already seen. Pagination complete.", flush=True)
break
# Use the timestamp of the last fill for the next page
last_timestamp = fills[-1].get("time")
current_start_time = last_timestamp
print(f" Continuing from timestamp: {datetime.fromtimestamp(last_timestamp / 1000).isoformat()}", flush=True)
# Print results
print("\n" + "=" * 80)
print("RESULTS")
print("=" * 80)
print(f"Total fills processed: {total_fill_count:,}")
print(f"Spot coin fills: {spot_fill_count:,}")
print(f"Total spot coin trading volume: {total_volume:,.4f}")
print(f"Pages fetched: {page_count}")
print("=" * 80)
def main():
parser = argparse.ArgumentParser(
description="Calculate total trading volume for spot coins on Hyperliquid"
)
parser.add_argument(
"--user",
type=str,
default=DEFAULT_USER,
help=f"User wallet address (default: {DEFAULT_USER})"
)
parser.add_argument(
"--api-key",
type=str,
required=True,
help="Allium API key"
)
args = parser.parse_args()
# Validate user address format (basic check)
if not args.user.startswith("0x") or len(args.user) != 42:
print("Error: Invalid user address format. Expected 0x followed by 40 hex characters.", file=sys.stderr)
sys.exit(1)
calculate_spot_volume(args.user, args.api_key)
if __name__ == "__main__":
main()
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
"time"
)
const (
apiURL = "https://api.allium.so/api/v1/developer/trading/hyperliquid/info/fills"
defaultUser = "0xc0142fc8aa609f324b44e414816ea549322afbe8"
startTimeMS = int64(1609459200000) // Jan 1, 2021
maxResults = 2000
requestTimeout = 30 * time.Second
)
// Fill represents a single fill from the API
type Fill struct {
ClosedPnl *string `json:"closedPnl"`
Coin *string `json:"coin"`
Crossed *bool `json:"crossed"`
Dir *string `json:"dir"`
Hash *string `json:"hash"`
Oid *int64 `json:"oid"`
Px *string `json:"px"`
Side *string `json:"side"`
StartPosition *string `json:"startPosition"`
Sz *string `json:"sz"`
Time *int64 `json:"time"`
Fee *string `json:"fee"`
FeeToken *string `json:"feeToken"`
Tid *int64 `json:"tid"`
BuilderFee *string `json:"builderFee"`
BuilderAddress *string `json:"builderAddress"`
TwapId *int64 `json:"twapId"`
}
// FillRequest represents the API request payload
type FillRequest struct {
Type string `json:"type"`
User string `json:"user"`
StartTime int64 `json:"startTime"`
EndTime *int64 `json:"endTime,omitempty"`
}
// FillKey uniquely identifies a fill
type FillKey struct {
Time int64
Coin string
Tid int64
}
func main() {
user := flag.String("user", defaultUser, "User wallet address")
apiKey := flag.String("api-key", "", "Allium API key")
flag.Parse()
if *apiKey == "" {
fmt.Fprintln(os.Stderr, "Error: --api-key is required")
flag.Usage()
os.Exit(1)
}
// Validate user address format
if !isValidAddress(*user) {
fmt.Fprintln(os.Stderr, "Error: Invalid user address format. Expected 0x followed by 40 hex characters.")
os.Exit(1)
}
calculateSpotVolume(*user, *apiKey)
}
func isValidAddress(address string) bool {
matched, _ := regexp.MatchString(`^0x[0-9a-fA-F]{40}$`, address)
return matched
}
func calculateSpotVolume(user, apiKey string) {
seenFills := make(map[FillKey]bool)
var totalVolume float64
var spotFillCount int
var totalFillCount int
currentStartTime := startTimeMS
pageCount := 0
fmt.Printf("Fetching fills for user: %s\n", user)
fmt.Printf("Starting from: %s\n", time.UnixMilli(startTimeMS).Format(time.RFC3339))
fmt.Println(strings.Repeat("-", 80))
for {
pageCount++
fmt.Printf("\nPage %d: Fetching fills from %s...\n", pageCount, time.UnixMilli(currentStartTime).Format(time.RFC3339))
fills, err := fetchFills(user, currentStartTime, apiKey)
if err != nil {
fmt.Fprintf(os.Stderr, "Error fetching fills: %v\n", err)
os.Exit(1)
}
if len(fills) == 0 {
fmt.Println("No more fills found.")
break
}
fmt.Printf(" Received %d fills\n", len(fills))
// Track new fills in this batch
newFills := 0
newSpotFills := 0
var pageVolume float64
for _, fill := range fills {
// Create unique key for this fill
if fill.Time == nil || fill.Coin == nil || fill.Tid == nil {
continue
}
fillKey := FillKey{
Time: *fill.Time,
Coin: *fill.Coin,
Tid: *fill.Tid,
}
// Skip if we've already seen this fill
if seenFills[fillKey] {
continue
}
seenFills[fillKey] = true
newFills++
totalFillCount++
coin := *fill.Coin
// Check if it's a spot coin (starts with '@')
if strings.HasPrefix(coin, "@") {
if fill.Sz == nil || fill.Px == nil {
fmt.Fprintf(os.Stderr, " Warning: Missing size or price for coin %s\n", coin)
continue
}
var size, price float64
if _, err := fmt.Sscanf(*fill.Sz, "%f", &size); err != nil {
fmt.Fprintf(os.Stderr, " Warning: Could not parse size '%s' for coin %s\n", *fill.Sz, coin)
continue
}
if _, err := fmt.Sscanf(*fill.Px, "%f", &price); err != nil {
fmt.Fprintf(os.Stderr, " Warning: Could not parse price '%s' for coin %s\n", *fill.Px, coin)
continue
}
volume := size * price // Trading volume = size * price
totalVolume += volume
pageVolume += volume
spotFillCount++
newSpotFills++
}
}
fmt.Printf(" New unique fills: %d (spot: %d, volume: %.4f)\n", newFills, newSpotFills, pageVolume)
// Check if we need to continue paging
if len(fills) < maxResults {
fmt.Println("\nReceived less than 2000 fills, pagination complete.")
break
}
if newFills == 0 {
fmt.Println("\nNo new fills found, all remaining fills already seen. Pagination complete.")
break
}
// Use the timestamp of the last fill for the next page
lastFill := fills[len(fills)-1]
if lastFill.Time == nil {
fmt.Fprintln(os.Stderr, "Error: Last fill has no timestamp")
os.Exit(1)
}
currentStartTime = *lastFill.Time
fmt.Printf(" Continuing from timestamp: %s\n", time.UnixMilli(currentStartTime).Format(time.RFC3339))
}
// Print results
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("RESULTS")
fmt.Println(strings.Repeat("=", 80))
fmt.Printf("Total fills processed: %s\n", formatNumber(totalFillCount))
fmt.Printf("Spot coin fills: %s\n", formatNumber(spotFillCount))
fmt.Printf("Total spot coin trading volume: %s\n", formatFloat(totalVolume))
fmt.Printf("Pages fetched: %d\n", pageCount)
fmt.Println(strings.Repeat("=", 80))
}
func fetchFills(user string, startTime int64, apiKey string) ([]Fill, error) {
fmt.Println(" Making API request...")
request := FillRequest{
Type: "userFillsByTime",
User: user,
StartTime: startTime,
}
jsonData, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-KEY", apiKey)
client := &http.Client{
Timeout: requestTimeout,
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var fills []Fill
if err := json.Unmarshal(body, &fills); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return fills, nil
}
func formatNumber(n int) string {
str := fmt.Sprintf("%d", n)
var result []rune
for i, r := range str {
if i > 0 && (len(str)-i)%3 == 0 {
result = append(result, ',')
}
result = append(result, r)
}
return string(result)
}
func formatFloat(f float64) string {
str := fmt.Sprintf("%.4f", f)
parts := strings.Split(str, ".")
intPart := parts[0]
decPart := parts[1]
var result []rune
for i, r := range intPart {
if i > 0 && (len(intPart)-i)%3 == 0 {
result = append(result, ',')
}
result = append(result, r)
}
return string(result) + "." + decPart
}
#!/usr/bin/env node
/**
* Fetch all fills for a Hyperliquid user and calculate total trading volume for spot coins.
* Spot coins are identified by names starting with '@'.
*/
const API_URL = 'https://api.allium.so/api/v1/developer/trading/hyperliquid/info/fills';
const DEFAULT_USER = '0xc0142fc8aa609f324b44e414816ea549322afbe8';
const START_TIME_MS = 1609459200000; // Jan 1, 2021
const MAX_RESULTS = 2000;
const REQUEST_TIMEOUT = 30000;
/**
* Fetch fills from the API for a given time range
*/
async function fetchFills(user, startTime, apiKey, endTime = null) {
const payload = {
user,
type: 'userFillsByTime',
startTime,
};
if (endTime) {
payload.endTime = endTime;
}
console.log(' Making API request...');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': apiKey,
},
body: JSON.stringify(payload),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const text = await response.text();
throw new Error(`API returned status ${response.status}: ${text}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
/**
* Calculate total trading volume for spot coins
*/
async function calculateSpotVolume(user, apiKey) {
const seenFills = new Set(); // Set of "time:coin:tid" strings
let totalVolume = 0.0;
let spotFillCount = 0;
let totalFillCount = 0;
let currentStartTime = START_TIME_MS;
let pageCount = 0;
console.log(`Fetching fills for user: ${user}`);
console.log(`Starting from: ${new Date(START_TIME_MS).toISOString()}`);
console.log('-'.repeat(80));
while (true) {
pageCount++;
console.log(`\nPage ${pageCount}: Fetching fills from ${new Date(currentStartTime).toISOString()}...`);
let fills;
try {
fills = await fetchFills(user, currentStartTime, apiKey);
} catch (error) {
console.error(`Error fetching fills: ${error.message}`);
process.exit(1);
}
if (!fills || fills.length === 0) {
console.log('No more fills found.');
break;
}
console.log(` Received ${fills.length} fills`);
// Track new fills in this batch
let newFills = 0;
let newSpotFills = 0;
let pageVolume = 0.0;
for (const fill of fills) {
// Create unique key for this fill
const fillKey = `${fill.time}:${fill.coin}:${fill.tid}`;
// Skip if we've already seen this fill
if (seenFills.has(fillKey)) {
continue;
}
seenFills.add(fillKey);
newFills++;
totalFillCount++;
const coin = fill.coin || '';
// Check if it's a spot coin (starts with '@')
if (coin.startsWith('@')) {
const sz = fill.sz || '0';
const px = fill.px || '0';
try {
const size = parseFloat(sz);
const price = parseFloat(px);
if (isNaN(size) || isNaN(price)) {
console.error(` Warning: Could not parse size '${sz}' or price '${px}' for coin ${coin}`);
continue;
}
const volume = size * price; // Trading volume = size * price
totalVolume += volume;
pageVolume += volume;
spotFillCount++;
newSpotFills++;
} catch (error) {
console.error(` Warning: Error processing coin ${coin}: ${error.message}`);
}
}
}
console.log(` New unique fills: ${newFills} (spot: ${newSpotFills}, volume: ${pageVolume.toFixed(4)})`);
// Check if we need to continue paging
if (fills.length < MAX_RESULTS) {
console.log('\nReceived less than 2000 fills, pagination complete.');
break;
}
if (newFills === 0) {
console.log('\nNo new fills found, all remaining fills already seen. Pagination complete.');
break;
}
// Use the timestamp of the last fill for the next page
const lastFill = fills[fills.length - 1];
currentStartTime = lastFill.time;
console.log(` Continuing from timestamp: ${new Date(currentStartTime).toISOString()}`);
}
// Print results
console.log('\n' + '='.repeat(80));
console.log('RESULTS');
console.log('='.repeat(80));
console.log(`Total fills processed: ${formatNumber(totalFillCount)}`);
console.log(`Spot coin fills: ${formatNumber(spotFillCount)}`);
console.log(`Total spot coin trading volume: ${formatFloat(totalVolume)}`);
console.log(`Pages fetched: ${pageCount}`);
console.log('='.repeat(80));
}
/**
* Format number with comma separators
*/
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
/**
* Format float with comma separators and 4 decimal places
*/
function formatFloat(num) {
const [intPart, decPart] = num.toFixed(4).split('.');
return formatNumber(intPart) + '.' + decPart;
}
/**
* Validate Ethereum address format
*/
function isValidAddress(address) {
return /^0x[0-9a-fA-F]{40}$/.test(address);
}
/**
* Parse command line arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const parsed = {
user: DEFAULT_USER,
apiKey: null,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--user' && i + 1 < args.length) {
parsed.user = args[++i];
} else if (arg === '--api-key' && i + 1 < args.length) {
parsed.apiKey = args[++i];
} else if (arg === '--help' || arg === '-h') {
console.log(`
Usage: node spot_volume.js [options]
Calculate total trading volume for spot coins on Hyperliquid
Options:
--user <address> User wallet address (default: ${DEFAULT_USER})
--api-key <key> Allium API key (required)
--help, -h Show this help message
`);
process.exit(0);
}
}
return parsed;
}
/**
* Main entry point
*/
async function main() {
const args = parseArgs();
if (!args.apiKey) {
console.error('Error: --api-key is required');
console.error('Use --help for usage information');
process.exit(1);
}
if (!isValidAddress(args.user)) {
console.error('Error: Invalid user address format. Expected 0x followed by 40 hex characters.');
process.exit(1);
}
await calculateSpotVolume(args.user, args.apiKey);
}
// Run main function
main().catch((error) => {
console.error(`Fatal error: ${error.message}`);
process.exit(1);
});