kornrunner/ccxt
PHP port of CCXT, giving Laravel and PHP applications a unified API to connect to dozens of cryptocurrency exchanges for market data, tickers, order books, balances, and trading. Fetch prices and account info across exchanges with one consistent interface, without writing separate integrations for e...
Install with Composer and pin the version, since this library is a snapshot of an older CCXT (Feb 2021):
composer require kornrunner/ccxt
First place to look is src/ccxt/Exchange.php and one exchange class you care about (src/ccxt/binance.php, src/ccxt/kraken.php, etc.). The Exchange base class exposes the uniform API — if a method works on one exchange, the calling convention is the same for all ~110 of them. Also skim the upstream CCXT manual at ccxt.github.io: the method semantics, parameter shapes, and options keys are all inherited from there, even though the PHP port uses snake_case (fetch_ticker, create_order) instead of camelCase.
Minimal first use case — read a public ticker with no credentials:
$exchange = new \ccxt\binance();
$ticker = $exchange->fetch_ticker('BTC/USDT');
echo $ticker['last'];
Once you trust the read path, add credentials and hit an authenticated endpoint:
$exchange = new \ccxt\kraken([
'apiKey' => config('services.kraken.key'),
'secret' => config('services.kraken.secret'),
'enableRateLimit' => true,
]);
$balances = $exchange->fetch_balance();
Store keys in .env, never in code; use read-only keys during development.
Wrap the exchange in a domain service. Don't sprinkle new \ccxt\binance() across controllers — inject a configured client and keep the CCXT array shape out of your domain:
final class BinanceClient
{
public function __construct(private \ccxt\Exchange $exchange) {}
public function lastPrice(string $symbol): string
{
return $this->exchange->fetch_ticker($symbol)['last'];
}
}
Bind it once in a service provider so swapping exchanges (Binance → Kraken, spot → futures) is a one-line change.
Normalise the return shapes early. CCXT returns loosely-typed associative arrays with numeric strings. Cast into your own DTOs (value objects for Price, Amount) before they reach business logic. This is what makes the library usable in a Laravel app rather than a script.
Use the options array for exchange behaviour, not for every call. Set defaultType => 'future', adjustForTimeDifference => true, and rate limits once at construction. Method-level overrides belong in the call site only when they genuinely vary per request.
Rate limiting and caching go together. Even with enableRateLimit => true, a dashboard hitting fetch_ticker per request will hammer the exchange. Cache tickers in Redis for a few seconds and cache fetch_balance / fetch_open_orders for longer. Treat fetch_markets() as a boot-time cache — it's the heaviest call and rarely changes.
Order lifecycle. Typical pattern: create_order → store the exchange order id → poll fetch_order on a queue job with backoff → reconcile against fetch_open_orders on a schedule. Don't poll in a web request; put it on a scheduled job.
Sandbox first. Where supported, $exchange->set_sandbox_mode(true) before testing create_order. Not every exchange implements it, and some implementations are stale — validate by placing a minimum-size order on a funded testnet account.
Error handling at the boundary. Catch \ccxt\BaseError subclasses (AuthenticationError, InsufficientFunds, RateLimitExceeded, NetworkError) and translate them into your own exceptions. Retry only idempotent reads; never blind-retry create_order — use a client order id (clientOrderId in params) to make it safely retryable.
params keys are outdated. Expect certain exchanges in this fork to be broken against live APIs. Pin the version in composer.lock, smoke-test each exchange you rely on, and be prepared to fork/patch individual exchange classes (they're plain PHP, one file each — this is the intended extension point).fetchTicker / createOrder. In this library it's fetch_ticker / create_order. This one detail burns most newcomers.0.1 + 0.2 as a quantity gets your order rejected or silently rounded. Always pass '0.10000000' and format with the exchange's precision/limits from fetch_markets().adjustForTimeDifference => true and run NTP on the host.fetch_markets() is expensive and cached server-side. Call once, persist the result, and refresh on a schedule — not in every request.enableRateLimit uses the exchange's own rateLimit hint, which may be stale. Add your own leaky bucket in front for production traffic.null and missing keys are normal. CCXT normalises loosely — $ticker['bid'] may be null, fetch_ohlcv rows are [timestamp, open, high, low, close, volume] arrays. Guard every access or use a normaliser.$this->client) rather than trying to infer errors from CCXT's messages — the underlying error is often the exchange's raw JSON in $e->getMessage().curl/TLS outbound access, and never in a context where the process can be killed mid-create_order without a client-order-id reconciliation step.How can I help you explore Laravel packages today?