tigusigalpa / tokenterminal-php
Production-ready PHP SDK for the Token Terminal API v2
Requires
- php: ^8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- phpstan/phpstan: ^1.10 || ^2.0
- phpunit/phpunit: ^10.0 || ^11.0
README
A PHP client for the Token Terminal API v2.
It covers every endpoint the API exposes today, keeps the awkward parts (retries, redirects after a project gets renamed, responses that are half data and half errors) out of your application code, and drops into Laravel without any wiring.
What you get
- All 24 endpoints: Assets, Projects, Market Sectors, Metrics and Datasets
- Typed request objects and an immutable
TokenTerminalResultfor responses - Partial responses kept intact, so the
errorsarray survives next to validdata - Retries with exponential backoff and jitter, and
Retry-Afteris respected - Any PSR-18 HTTP client works; Guzzle is just the default
- One exception class per HTTP status, so you can catch exactly what you care about
- 308 redirects followed automatically when a project or asset is renamed
- Laravel service provider, facade, publishable config, auto-discovery
TokenTerminalConfigis immutable; change it withwith*()methods
Requirements
- PHP 8.1+
- ext-json
- Guzzle (pulled in by default, swap it for any PSR-18 client)
Installation
composer require tigusigalpa/tokenterminal-php
Quick start
use Tigusigalpa\TokenTerminal\TokenTerminalClient; $client = TokenTerminalClient::make('your-api-key'); $result = $client->projects()->all(); foreach ($result->data() as $project) { echo $project['name'] . ' (id: ' . $project['project_id'] . ")\n"; }
Configuration
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
TOKEN_TERMINAL_API_KEY |
Yes | — | Your Token Terminal API key |
TOKEN_TERMINAL_BASE_URL |
No | https://api.tokenterminal.com |
Custom base URL |
TOKEN_TERMINAL_TIMEOUT |
No | 30 |
HTTP timeout in seconds |
TOKEN_TERMINAL_RETRY_ATTEMPTS |
No | 3 |
Maximum retry attempts |
TOKEN_TERMINAL_RETRY_DELAY |
No | 1 |
Initial retry delay in seconds |
Once those are set, this is all you need:
$client = TokenTerminalClient::fromEnv();
Building the config by hand
$config = new TokenTerminalConfig( apiKey: 'your-key', baseUrl: 'https://api.tokenterminal.com', timeout: 30, retryAttempts: 3, retryDelay: 1, ); // Nothing is mutated; you always get a new instance back $config = $config->withRetry(5, 2); $config = $config->withRetryPOST(true); $client = new TokenTerminalClient($config);
Endpoints
| Domain | Method | HTTP Route |
|---|---|---|
| Assets | $client->assets()->all() |
GET /v2/assets |
| Assets | $client->assets()->get($id) |
GET /v2/assets/{asset_id} |
| Assets | $client->assets()->historicalMetrics($id, $req) |
GET /v2/assets/{asset_id}/metrics |
| Assets | $client->assets()->metricsBreakdown($id, $req) |
POST /v2/assets/{asset_id}/metrics-breakdown |
| Projects | $client->projects()->all() |
GET /v2/projects |
| Projects | $client->projects()->get($id) |
GET /v2/projects/{project_id} |
| Projects | $client->projects()->product($pid, $prodId) |
GET /v2/projects/{project_id}/products/{product_id} |
| Projects | $client->projects()->financialStatement($id, $req) |
GET /v2/projects/{project_id}/financial-statement |
| Projects | $client->projects()->historicalMetrics($id, $req) |
GET /v2/projects/{project_id}/metrics |
| Projects | $client->projects()->productHistoricalMetrics($pid, $prodId, $req) |
GET /v2/projects/{project_id}/products/{product_id}/metrics |
| Projects | $client->projects()->metricAggregations($id) |
GET /v2/projects/{project_id}/metric-aggregations |
| Market Sectors | $client->marketSectors()->all() |
GET /v2/market-sectors |
| Market Sectors | $client->marketSectors()->get($id) |
GET /v2/market-sectors/{market_sector_id} |
| Metrics | $client->metrics()->all() |
GET /v2/metrics |
| Metrics | $client->metrics()->data($id, $req) |
GET /v2/metrics/{metric_id} |
| Metrics | $client->metrics()->aggregations($req) |
GET /v2/metric-aggregations |
| Metrics | $client->metrics()->breakdown($req) |
POST /v2/metric-breakdown |
| Datasets | $client->datasets()->all() |
GET /v2/datasets |
| Datasets | $client->datasets()->blockchainComparison($req) |
GET /v2/datasets/blockchain_comparison |
| Datasets | $client->datasets()->cohortAnalysis($req) |
GET /v2/datasets/cohort_analysis |
| Datasets | $client->datasets()->cryptoScreener($req) |
GET /v2/datasets/crypto_screener |
| Datasets | $client->datasets()->insiderTransactions($req) |
GET /v2/datasets/insider_transactions |
| Datasets | $client->datasets()->projectContracts($req) |
GET /v2/datasets/project_contracts |
| Datasets | $client->datasets()->trendingContracts($req) |
GET /v2/datasets/trending_contracts |
Errors
Catch the specific case you can actually handle, and let ApiException cover the rest:
use Tigusigalpa\TokenTerminal\Exceptions\UnauthorizedException; use Tigusigalpa\TokenTerminal\Exceptions\RateLimitException; use Tigusigalpa\TokenTerminal\Exceptions\NotFoundException; use Tigusigalpa\TokenTerminal\Exceptions\ApiException; try { $result = $client->projects()->all(); } catch (UnauthorizedException $e) { echo "Invalid API key\n"; } catch (RateLimitException $e) { printf("Rate limited. Retry-After: %s\n", $e->getRetryAfter()); } catch (NotFoundException $e) { echo "Not found\n"; } catch (ApiException $e) { printf("API error %d: %s\n", $e->getStatusCode(), $e->getMessage()); }
The hierarchy:
TokenTerminalException
├── ConfigurationException
├── TransportException
└── ApiException
├── BadRequestException (400)
├── UnauthorizedException (401)
├── SubscriptionException (402)
├── ForbiddenException (403)
├── NotFoundException (404)
└── RateLimitException (429)
Partial success
Some endpoints happily return usable data together with an errors array, for example when one metric id out of five is wrong. Instead of throwing everything away, the SDK hands you both:
$result = $client->projects()->historicalMetrics('uniswap', new HistoricalMetricsRequest( metricIds: ['fees', 'invalid-metric'], )); foreach ($result->errors() as $error) { printf("Partial error: %s %s=%s\n", $error->getCode(), $error->getField(), $error->getValue()); }
How retries behave
- GET requests are retried on 429, 5xx and transport failures.
- POST requests are not retried, unless you opt in with
withRetryPOST(true). - Delays grow exponentially, are capped, and get a bit of jitter so parallel workers don't sync up.
- If the API sends
Retry-After, that value wins.
Laravel
Auto-discovery takes care of the service provider. Publish the config if you want to tweak it:
php artisan vendor:publish --tag=tokenterminal-config
use Tigusigalpa\TokenTerminal\Laravel\Facades\TokenTerminal; $projects = TokenTerminal::projects()->all();
There's more detail in examples/laravel_usage.md.
Examples
| Example | What it shows | Run it |
|---|---|---|
| Basic | Listing projects and metrics | php examples/basic.php |
| Project Metrics | Historical metrics with filters | php examples/project_metrics.php |
| Metric Breakdown | POST breakdown with stats | php examples/metric_breakdown.php |
| Datasets | Querying the crypto screener | php examples/datasets.php |
| Error Handling | Dealing with API errors | php examples/error_handling.php |
| Laravel Usage | Laravel integration guide | See examples/laravel_usage.md |
Testing
composer install vendor/bin/phpunit vendor/bin/phpstan analyse src --level=5 composer validate --strict
Everything runs against Guzzle's MockHandler, so the test suite never touches the real API and needs no API key.
API documentation
License
MIT © Igor Sazonov