-
Notifications
You must be signed in to change notification settings - Fork 0
Recipes
Practical, copyable patterns for cryptohopper/sdk (Composer). Every snippet runs as-is — paste into a PHP file and execute with php file.php. They use only the public SDK surface, never internals.
The SDK is synchronous and built on Guzzle. Responses are decoded JSON arrays — access fields with ['key'], not arrow notation.
composer require cryptohopper/sdk- Build the client and make a single call
- Wait for a backtest to finish
- Find every open position across all your hoppers
- Detect new fills since the last poll
- Match on CryptohopperException codes (PHP 8 match)
- Fail fast on auth errors, retry on transient ones
- Read your remaining backtest quota
- Bring your own Guzzle client (proxies, middleware, mTLS)
- Tighten timeouts for short-lived workers
- Disable the SDK's built-in retry and handle 429 yourself
- Mock the SDK in PHPUnit tests with a Guzzle MockHandler
<?php
require __DIR__ . '/vendor/autoload.php';
use Cryptohopper\Sdk\Client;
$ch = new Client(apiKey: getenv('CRYPTOHOPPER_TOKEN'));
$me = $ch->user->get();
echo $me['email'], PHP_EOL;The SDK uses PHP 8.1+ named arguments throughout — new Client(apiKey: ..., timeout: 8, maxRetries: 1) is the idiomatic call style.
Backtests run async server-side. create returns immediately with an ID; you poll get until status is terminal.
function runBacktest(Client $ch, int|string $hopperId, string $fromDate, string $toDate): array {
$bt = $ch->backtest->create([
'hopper_id' => $hopperId,
'start_date' => $fromDate,
'end_date' => $toDate,
]);
while (true) {
$cur = $ch->backtest->get($bt['id']);
if (in_array($cur['status'] ?? '', ['completed', 'failed'], true)) {
return $cur;
}
sleep(5);
}
}The backtest rate bucket is separate (1 request per 2 seconds). 5-second polling stays well clear.
foreach ($ch->hoppers->list() as $h) {
foreach ($ch->hoppers->positions($h['id']) as $p) {
printf(
"%s (#%s): %s %s @ %s\n",
$h['name'] ?? '?', $h['id'], $p['amount'] ?? '?', $p['coin'] ?? '?', $p['rate'] ?? '?',
);
}
}This is sequential — one request per hopper. PHP doesn't have built-in async; if you need parallel fan-out, see Guzzle's Pool (next-but-one recipe) for true concurrency.
$seen = [];
while (true) {
foreach ($ch->hoppers->orders($hopperId) as $o) {
$id = $o['id'] ?? null;
if ($id === null || isset($seen[$id])) {
continue;
}
if (($o['status'] ?? '') === 'filled') {
$seen[$id] = true;
printf("Fill: %s %s %s @ %s\n", $o['market'], $o['type'], $o['amount'], $o['price']);
}
}
sleep(10);
}For production-grade fill notifications, configure the webhooks resource — push beats poll for event delivery.
getCode() is shadowed by PHP's Exception::getCode() (returns int). The string code lives on getErrorCode().
use Cryptohopper\Sdk\Exceptions\CryptohopperException;
try {
$ch->hoppers->get('999999999');
} catch (CryptohopperException $e) {
match ($e->getErrorCode()) {
'NOT_FOUND' => print("no such hopper\n"),
'UNAUTHORIZED', 'FORBIDDEN' => printf("auth problem; IP we sent: %s\n", $e->getIpAddress() ?? 'unknown'),
'RATE_LIMITED' => printf("rate limited; retry after %dms\n", $e->getRetryAfterMs() ?? 0),
default => throw $e,
};
}$e->getErrorCode() is a stable string — compare with ===, never substring-match.
The SDK auto-retries 429s. For 5xx and network errors you may want a tighter retry. Auth errors should never be retried.
function withRetry(callable $fn, int $maxAttempts = 3): mixed {
$attempt = 0;
while (true) {
$attempt++;
try {
return $fn();
} catch (CryptohopperException $e) {
if (in_array($e->getErrorCode(), ['UNAUTHORIZED', 'FORBIDDEN', 'NOT_FOUND', 'VALIDATION_ERROR'], true)) {
throw $e;
}
if ($attempt >= $maxAttempts) {
throw $e;
}
usleep(500_000 * (2 ** ($attempt - 1)));
}
}
}
$me = withRetry(fn () => $ch->user->get());$limits = $ch->backtest->limits();
printf("Backtests remaining: %s of %s\n", $limits['remaining'] ?? '?', $limits['limit'] ?? '?');For the normal and order buckets there's no explicit quota endpoint — the only signal is Retry-After on a 429 (read it via $e->getRetryAfterMs()).
Every option you can pass to GuzzleHttp\Client::__construct is yours.
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Log\LoggerInterface;
use Cryptohopper\Sdk\Client;
$stack = HandlerStack::create();
$stack->push(Middleware::log($logger, new \GuzzleHttp\MessageFormatter('{method} {uri} -> {code} ({phrase})')));
$guzzle = new GuzzleClient([
'handler' => $stack,
'proxy' => 'http://corp-proxy:8080',
'verify' => '/etc/ssl/certs/corp-ca.pem',
]);
$ch = new Client(
apiKey: getenv('CRYPTOHOPPER_TOKEN'),
httpClient: $guzzle,
);When you pass httpClient, the SDK uses it as-is — timeout and base URL still apply (set per-request). Don't set conflicting global options on your Guzzle client.
Default timeout is 30 seconds. Inside an AWS Lambda PHP runtime (15s) or any short-lived worker, drop it.
$ch = new Client(
apiKey: getenv('CRYPTOHOPPER_TOKEN'),
timeout: 8, // ~half your function budget
maxRetries: 1, // leave room for one retry inside the function lifetime
);A CryptohopperException with getErrorCode() === 'TIMEOUT' is much easier to handle than a process kill.
$ch = new Client(
apiKey: getenv('CRYPTOHOPPER_TOKEN'),
maxRetries: 0,
);
try {
$ch->hoppers->list();
} catch (CryptohopperException $e) {
if ($e->getErrorCode() === 'RATE_LIMITED') {
printf("rate limited; server says wait %dms\n", $e->getRetryAfterMs() ?? 0);
// your custom queue / circuit breaker / etc.
} else {
throw $e;
}
}Useful when you have your own queue, want exact backoff control, or are running inside something that already does retries (Laravel queues, Symfony Messenger).
The SDK accepts any Psr\Http\Client\ClientInterface. Plug in Guzzle's MockHandler.
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Cryptohopper\Sdk\Client;
class UserTest extends TestCase
{
public function test_user_get(): void
{
$mock = new MockHandler([
new Response(200, [], json_encode(['data' => ['id' => 42, 'email' => 'alice@example.com']])),
]);
$stack = HandlerStack::create($mock);
$http = new GuzzleClient(['handler' => $stack]);
$ch = new Client(apiKey: 'test', httpClient: $http);
$me = $ch->user->get();
self::assertSame(42, $me['id']);
}
public function test_retry_on_429(): void
{
$mock = new MockHandler([
new Response(429, ['Retry-After' => '0']),
new Response(200, [], json_encode(['data' => ['id' => 42]])),
]);
$http = new GuzzleClient(['handler' => HandlerStack::create($mock)]);
$ch = new Client(apiKey: 'test', httpClient: $http);
self::assertSame(42, $ch->user->get()['id']);
}
}The SDK pulls data out of the envelope automatically — your mock returns {"data": ...}, your assertion sees the inner value.
Pages
Other SDKs
Resources