tigusigalpa / glassnode-php
Framework-agnostic PHP SDK for the Glassnode Basic API with a first-class Laravel bridge.
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0 || ^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.0
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- phpunit/phpunit: ^9.5 || ^10.0 || ^11.0
README
Unofficial PHP SDK for the Glassnode Basic API. Not affiliated with or endorsed by Glassnode.
A framework-agnostic, production-oriented PHP library for the Glassnode Basic API with a first-class Laravel bridge.
If you've ever tried to pull on-chain metrics from Glassnode in a PHP project, you know the drill: hand-rolling cURL requests, manually constructing query strings, decoding JSON, and wrapping everything in try-catch blocks just to get a clean error message. This SDK takes care of all that — and goes further by making the entire documented metric surface discoverable and safely consumable through three layers of API:
- Transport/config —
GlassnodeClientwith cURL or PSR-18 transport, immutable config, automatic retries, and rate-limit awareness - Metadata/generic —
MetadataResourcefor runtime discovery,GlassnodeClient::get()for any metric path (present or future) - Ergonomic category resources — 25 typed resource classes with convenience methods that map 1:1 to Glassnode's endpoint categories
The result? You write less boilerplate, catch errors earlier, and spend more time analyzing data instead of wrestling with HTTP plumbing.
Why This SDK?
- Stop hand-writing HTTP calls — every endpoint is a method call away with proper parameter handling
- Discover metrics at runtime — not sure which parameters a metric accepts? Ask the metadata API
- Survive rate limits gracefully — automatic retry on 429 with server-aware backoff
- Stay framework-agnostic — works in plain PHP, Laravel, Symfony, or anything else
- Keep your API key safe — header-based auth by default, never leaked in URLs or logs
- Only pay for what you use — bulk endpoints with explicit asset lists so you control credit consumption
Features
- Framework-agnostic — works in any PHP 8.1+ project, no framework required
- First-class Laravel bridge — service provider, facade, config publishing, auto-discovery
- 25 category resources with typed convenience methods covering every documented endpoint category
- Generic metric API for every valid metric path — present or future, even ones not yet wrapped
- Metadata-first — discover assets, metric paths, and parameter capabilities at runtime before making data calls
- Bulk endpoint support with repeated query parameters for multi-asset requests
- Point-in-Time metrics with
computed_attimestamp preservation for historically accurate analysis - Header authentication (
X-Api-Key) by default; query-string opt-in for environments that need it - Automatic retry on HTTP 429 with
x-rate-limit-resetsupport and exponential backoff - Rich exception hierarchy —
BadRequestException,UnauthorizedException,NotFoundException,RateLimitException, and more, each carrying context about what went wrong - PSR-18 compatible — use any PSR-18 HTTP client (Guzzle, Symfony HTTP Client, etc.), or the built-in zero-dependency cURL transport
- Immutable configuration — once created, config objects can't be accidentally mutated mid-request
- No network requests at boot time — Laravel-safe, won't slow down your app startup
Installation
composer require tigusigalpa/glassnode-php
That's it. The package has no hard dependencies beyond ext-curl and ext-json (both enabled by default in virtually
every PHP installation). If you're using Laravel, the service provider and facade are auto-discovered — no manual
registration needed.
Laravel Setup
The package auto-discovers the service provider and facade via Laravel's package discovery. Publish the config file to customize defaults:
php artisan vendor:publish --tag=glassnode-config
This creates config/glassnode.php in your project. Then set your API key in .env:
GLASSNODE_API_KEY=your-api-key-here
The published config file lets you override the base URL, timeout, retry settings, auth mode, and more — all in one place.
Plain PHP Setup
No framework? No problem. Just instantiate the client directly:
use Tigusigalpa\Glassnode\GlassnodeClient; use Tigusigalpa\Glassnode\GlassnodeConfig; $config = new GlassnodeConfig(apiKey: 'your-api-key'); $client = new GlassnodeClient($config);
Or load from environment variables (GLASSNODE_API_KEY, GLASSNODE_TIMEOUT, etc.):
$config = GlassnodeConfig::fromEnv(); $client = new GlassnodeClient($config);
Quick Start
Standalone PHP
Here's a complete example — fetch BTC price data and print it:
use Tigusigalpa\Glassnode\GlassnodeClient; use Tigusigalpa\Glassnode\GlassnodeConfig; $client = new GlassnodeClient(new GlassnodeConfig( apiKey: 'YOUR_API_KEY', timeout: 15.0, retryAttempts: 3, )); // Fetch BTC price with 24h resolution $price = $client->market->price(['a' => 'BTC', 'i' => '24h']); foreach ($price as $point) { printf("BTC price at %d: $%.2f\n", $point['t'], $point['v']); }
Want to explore what's available before pulling data? Use the metadata API:
// List all supported assets $assets = $client->metadata->assets(); foreach ($assets as $asset) { printf("%s (%s)\n", $asset['name'], $asset['symbol']); } // Inspect a specific metric's parameters $metricInfo = $client->metadata->metric('/market/price_usd'); print_r($metricInfo);
Laravel
In Laravel, you can use the facade for quick access:
use Tigusigalpa\Glassnode\Laravel\Facades\Glassnode; // Fetch metrics with a clean, expressive syntax $price = Glassnode::market()->price(['a' => 'BTC', 'i' => '24h']); $sopr = Glassnode::indicators()->sopr(['a' => 'BTC']); $assets = Glassnode::metadata()->assets();
Or via dependency injection (recommended for controllers and services):
use Tigusigalpa\Glassnode\GlassnodeClient; class DashboardController extends Controller { public function index(GlassnodeClient $client) { $price = $client->market->price(['a' => 'BTC']); $activeAddresses = $client->addresses->activeCount(['a' => 'BTC']); return view('dashboard', [ 'price' => $price, 'activeAddresses' => $activeAddresses, ]); } }
Common Use Cases
Building a market dashboard:
// Get OHLC candles for charting $ohlc = $client->market->priceOhlc(['a' => 'BTC', 'i' => '1h']); // Market cap and realized cap for valuation analysis $mcap = $client->market->marketCap(['a' => 'BTC']); $rcap = $client->market->realizedCap(['a' => 'BTC']); // MVRV ratio — a classic cycle indicator $mvrv = $client->indicators->mvrv(['a' => 'BTC']);
Monitoring network health:
// Active addresses — measures network usage $active = $client->addresses->activeCount(['a' => 'BTC', 'i' => '24h']); // Hash rate — mining security $hashrate = $client->mining->hashRateMean(['a' => 'BTC']); // Total supply — track inflation $supply = $client->supply->current(['a' => 'BTC']);
API Key Security
Your Glassnode API key is the gateway to your account and its data credits. Treat it like a password.
- Never hardcode your API key in source code, commit it to version control, or expose it in client-side code
- Use
GlassnodeConfig::fromEnv()in plain PHP or Laravel'senv('GLASSNODE_API_KEY')to load from environment variables - The SDK uses header mode (
X-Api-Key) by default — your key is sent in an HTTP header and never appears in URLs, query strings, or server access logs - Query-string mode (
api_keyparameter) is available as an opt-in for environments where headers aren't supported, but it's less secure — the key may be visible in proxy logs, browser history, or referrer headers - If you accidentally expose a key, rotate it immediately in Glassnode Studio settings
Authentication Modes
The SDK supports two authentication modes. Header mode is the default and recommended for all production usage:
// Header mode (default, recommended) // Sends the key as an X-Api-Key header — invisible in URLs and logs new GlassnodeConfig(apiKey: 'key') // Query-string mode (opt-in, less secure) // Appends ?api_key=... to every request URL new GlassnodeConfig(apiKey: 'key', authMode: GlassnodeConfig::AUTH_MODE_QUERY)
Configuration
The GlassnodeConfig object is immutable — once created, its values can't change. This prevents accidental mutation
mid-request in long-running processes.
All configuration options with their defaults:
$config = new GlassnodeConfig( apiKey: 'YOUR_API_KEY', // Required — your Glassnode API key baseUrl: 'https://api.glassnode.com', // API base URL (rarely needs changing) timeout: 15.0, // HTTP timeout in seconds retryAttempts: 3, // Max retries on HTTP 429 retryDelay: 1.0, // Base delay in seconds for exponential backoff authMode: 'header', // 'header' (default) or 'query' userAgent: 'glassnode-php/1.0', // User-Agent header sent with each request appId: null, // Optional app identifier for Glassnode analytics );
Creating Config from Different Sources
From an array (great for loading from a config file):
$config = GlassnodeConfig::fromArray([ 'api_key' => 'YOUR_API_KEY', 'timeout' => 15.0, 'retry_attempts' => 3, ]);
From environment variables (reads GLASSNODE_API_KEY, GLASSNODE_TIMEOUT, etc.):
$config = GlassnodeConfig::fromEnv();
In Laravel, the published config/glassnode.php file handles all of this for you — just set the values in .env and
the service provider wires everything up automatically.
Category Resources
| Resource | Client Property | Laravel Facade | Documentation |
|---|---|---|---|
| Addresses | $client->addresses |
Glassnode::addresses() |
Addresses |
| Bridges | $client->bridges |
Glassnode::bridges() |
Bridges |
| Blockchain | $client->blockchain |
Glassnode::blockchain() |
Blockchain |
| Breakdowns | $client->breakdowns |
Glassnode::breakdowns() |
Breakdowns |
| DeFi | $client->defi |
Glassnode::defi() |
DeFi |
| Derivatives | $client->derivatives |
Glassnode::derivatives() |
Derivatives |
| Distribution | $client->distribution |
Glassnode::distribution() |
Distribution |
| Entities | $client->entities |
Glassnode::entities() |
Entities |
| ETH 2.0 | $client->eth2 |
Glassnode::eth2() |
ETH 2.0 |
| Fees | $client->fees |
Glassnode::fees() |
Fees |
| Global | $client->global |
Glassnode::global() |
Global |
| Indicators | $client->indicators |
Glassnode::indicators() |
Indicators |
| Institutions | $client->institutions |
Glassnode::institutions() |
Institutions |
| Lightning | $client->lightning |
Glassnode::lightning() |
Lightning |
| Macro | $client->macro |
Glassnode::macro() |
Macro |
| Market | $client->market |
Glassnode::market() |
Market |
| Mempool | $client->mempool |
Glassnode::mempool() |
Mempool |
| Mining | $client->mining |
Glassnode::mining() |
Mining |
| Options | $client->options |
Glassnode::options() |
Options |
| Point-In-Time | $client->pointInTime |
Glassnode::pointInTime() |
PIT |
| Protocols | $client->protocols |
Glassnode::protocols() |
Protocols |
| Signals | $client->signals |
Glassnode::signals() |
Signals |
| Supply | $client->supply |
Glassnode::supply() |
Supply |
| Transactions | $client->transactions |
Glassnode::transactions() |
Transactions |
| Treasuries | $client->treasuries |
Glassnode::treasuries() |
Treasuries |
Full endpoint coverage: docs/endpoint-coverage.md
Generic Metric API
The 25 category resources cover every documented endpoint category, but Glassnode occasionally adds new metrics before a typed wrapper exists. The generic API ensures you're never blocked — it works with any valid metric path, present or future:
// Raw decoded JSON — works with any metric path $data = $client->get('/v1/metrics/addresses/sending_count', ['a' => 'BTC']); // Full parameter set: asset, resolution, time range $data = $client->get('/v1/metrics/market/price_ohlc', [ 'a' => 'BTC', // Asset 'i' => '24h', // Resolution: 10m, 1h, 24h, 1w, 1month 's' => 1609459200, // Since (Unix timestamp) 'u' => 1609545600, // Until (Unix timestamp) 'c' => 'USD', // Currency (for market metrics) 'f' => 'Json', // Format: Json, CSV ]);
Common Query Parameters
| Parameter | Description | Example |
|---|---|---|
a |
Asset symbol | 'BTC', 'ETH' |
i |
Resolution interval | '10m', '1h', '24h', '1w', '1month' |
s |
Since (Unix timestamp) | 1609459200 |
u |
Until (Unix timestamp) | 1609545600 |
c |
Currency (market metrics) | 'USD', 'EUR' |
f |
Response format | 'Json', 'CSV' |
timestamp_format |
Timestamp format | 'unix', 'humanized', 'humanized-30d' |
Use metadata/metric to discover which parameters a specific metric accepts.
Bulk Metrics
Bulk endpoints let you fetch data for multiple assets in a single request, saving on rate limits. However, credits are still consumed per asset — so a bulk call for 5 assets costs the same as 5 individual calls.
// Fetch MVRV for BTC and ETH in one call $resp = $client->get('/v1/metrics/market/mvrv/bulk', [ 'a' => ['BTC', 'ETH'], // Multiple assets as an array 's' => strtotime('-7 days'), 'i' => '24h', ]);
Bulk Best Practices
- Always specify assets explicitly — never use wildcards, as this can consume unexpected credits
- Check
bulk_supportedviametadata/metricbefore calling bulk endpoints — not all metrics support it - Batch in reasonable sizes — 5–10 assets per call is a good balance between throughput and credit visibility
- Monitor your credit usage with
$client->user->apiUsage()to catch unexpected consumption early
Error Handling
The SDK throws a rich hierarchy of exceptions, so you can catch exactly the errors you care about without parsing error messages or HTTP status codes yourself:
use Tigusigalpa\Glassnode\Exceptions\BadRequestException; use Tigusigalpa\Glassnode\Exceptions\UnauthorizedException; use Tigusigalpa\Glassnode\Exceptions\NotFoundException; use Tigusigalpa\Glassnode\Exceptions\RateLimitException; use Tigusigalpa\Glassnode\Exceptions\GlassnodeException; try { $data = $client->indicators->sopr(['a' => 'BTC']); } catch (UnauthorizedException $e) { // 401 — API key is missing, invalid, or expired echo "Invalid API key — check your configuration\n"; } catch (RateLimitException $e) { // 429 — you've hit the rate limit (after all retries are exhausted) printf("Rate limited. Reset in %s seconds.\n", $e->rateLimitReset ?? 'unknown'); } catch (BadRequestException $e) { // 400 — invalid parameters, unsupported asset, etc. printf("Bad request: %s\n", $e->getMessage()); } catch (NotFoundException $e) { // 404 — the metric path doesn't exist echo "Metric not found — check the path with metadata/metric\n"; } catch (GlassnodeException $e) { // Catch-all for any other SDK error (network issues, transport errors, etc.) printf("Unexpected error: %s\n", $e->getMessage()); }
Exception Hierarchy
All exceptions extend GlassnodeException, so you can catch everything with a single catch block if you prefer:
GlassnodeException (base)
├── BadRequestException (HTTP 400)
├── UnauthorizedException (HTTP 401)
├── NotFoundException (HTTP 404)
├── RateLimitException (HTTP 429, after retries)
├── ConfigurationException (invalid config)
└── TransportException (network/HTTP errors)
Each exception carries the HTTP status code, response body, and (for RateLimitException) the rateLimitReset value
from the x-rate-limit-reset header.
PSR-18 HTTP Client Support
The SDK ships with a zero-dependency cURL transport that works out of the box. But if you're already using Guzzle, Symfony HTTP Client, or any other PSR-18 compatible client in your project, you can plug it in directly — no need for a second HTTP dependency:
use GuzzleHttp\Client; use GuzzleHttp\Psr7\HttpFactory; $client = new GlassnodeClient( config: $config, psrClient: new Client(), // Any PSR-18 ClientInterface requestFactory: new HttpFactory(), // PSR-17 RequestFactoryInterface streamFactory: new HttpFactory(), // PSR-17 StreamFactoryInterface );
This is particularly useful in Symfony projects (which ship with their own HTTP client) or in projects where you need custom middleware like request logging, circuit breakers, or proxy configuration.
Retry Behavior
Nobody likes getting rate-limited. The SDK handles 429 responses automatically so you don't have to wrap every call in retry logic:
- Retries only
GETrequests on HTTP 429 — non-idempotent methods are never retried - Honors
x-rate-limit-resetheader when present — waits exactly as long as the server tells us - Falls back to exponential backoff when the header is absent:
retryDelay * 2^attempt - Never retries 400, 401, or 404 — these are client errors that won't resolve by retrying
- Configurable via
retryAttempts(default: 3) andretryDelay(default: 1.0s)
If all retry attempts are exhausted, a RateLimitException is thrown with the rateLimitReset value (if available) so
your application can decide how to handle it — queue the request, alert the user, or back off further.
Rate Limits
Rate limits are governed by Glassnode's servers and depend on your subscription tier. The API returns these headers on every response, so you can monitor your usage proactively:
| Header | Description |
|---|---|
x-rate-limit-limit |
Total request limit per minute (e.g. 600 for standard tier) |
x-rate-limit-remaining |
Requests remaining in the current window |
x-rate-limit-reset |
Seconds until the limit resets |
Metadata endpoints are separately limited to 1200 req/min, so you can discover metrics freely without worrying about impacting your data request budget.
Tips for Staying Within Limits
- Cache metadata responses — assets and metric definitions rarely change
- Use appropriate resolutions — don't fetch 10-minute data when you only need daily aggregates
- Batch with bulk endpoints where supported — one HTTP call instead of many
- Monitor
x-rate-limit-remainingand back off before hitting zero
Data Credits
Glassnode charges data credits per request, not per data point. Understanding the credit model helps you avoid surprises:
- BTC: 1 credit per request
- All other assets: 2 credits per request
- Bulk endpoints: credits = sum of individual calls (e.g., 5 assets = 5× credits)
- Monitor usage via
$client->user->apiUsage()or Studio settings
If you're building a dashboard that pulls many metrics, consider caching results and refreshing on a schedule rather than polling continuously.
Testing
The test suite uses mocked HTTP transports — no API key or live requests are required, so you can run the full suite anywhere:
composer install vendor/bin/phpunit
For verbose output with test names:
vendor/bin/phpunit --testdox
The suite includes 41 tests (37 unit + 4 feature) covering all resources, error handling, retry logic, configuration, and the Laravel bridge.
Examples
The examples/ directory contains ready-to-run scripts you can adapt for your own projects:
- Basic usage — price, indicators, assets, API usage
- Laravel — facade and dependency injection in a controller
- Error handling — exception types and recovery strategies
Compatibility
- PHP 8.1+ (uses named arguments, readonly properties, enums)
- Laravel 10, 11, 12, 13 (auto-discovery, facade, config publishing)
- ext-curl and ext-json required (both enabled by default in standard PHP)
- PSR-18 client optional (for custom HTTP transports like Guzzle or Symfony HTTP Client)
FAQ
Do I need a Glassnode account to use this SDK?
Yes. You need a Glassnode account with an API key. Sign up at studio.glassnode.com — there's a free tier with limited credits to get started.
Is this an official Glassnode product?
No. This is an unofficial, community-built SDK. It's not affiliated with or endorsed by Glassnode. The official API documentation is at docs.glassnode.com.
Does the SDK work without Laravel?
Absolutely. The core library is framework-agnostic. The Laravel bridge is an optional layer on top — if you're using
plain PHP, Symfony, or any other framework, just instantiate GlassnodeClient directly.
What happens when Glassnode adds new metrics?
The generic API ($client->get()) works with any valid metric path, so you can use new metrics immediately even before
a typed wrapper is added. Check metadata/metrics to discover new paths at runtime.
Can I use this in a long-running process (queue worker, daemon)?
Yes. The GlassnodeConfig is immutable, and the GlassnodeClient is safe to reuse across requests. The cURL transport
creates fresh handles per request, so there's no state leakage between calls.
How do I get CSV format instead of JSON?
Pass 'f' => 'CSV' in the query parameters. The SDK returns the raw CSV string as-is. JSON (the default) is
automatically decoded into PHP arrays.
Contributing
Contributions are welcome! Whether it's a bug fix, a new example, improved documentation, or a feature — here's how to get started:
- Fork the repository and create your branch from
main - Run tests to make sure everything passes before you start:
vendor/bin/phpunit - Make your changes — keep code style consistent with the existing codebase
- Add tests for any new functionality — all tests must pass with mocked transports (no live API calls)
- Submit a pull request with a clear description of what and why
Reporting Issues
Found a bug or have a feature request? Please open an issue on GitHub with:
- A clear description of the problem or request
- Steps to reproduce (for bugs)
- Expected vs. actual behavior
- PHP version and framework (if applicable)
Changelog
See CHANGELOG.md for version history and breaking changes.
Author
Igor Sazonov — sovletig@gmail.com — github.com/tigusigalpa
License
MIT — do whatever you want, just don't blame me if something breaks.