Framework-agnostic Composer package for connecting to cryptocurrency exchanges. A single Exchange contract lets consumers fetch market data across multiple exchanges (Bybit, Binance, OKX, ...), where each exchange is an interchangeable driver — consumers program against the contract, never against a specific exchange.
v0 scope: klines() only (public candlestick data, no authentication). Balance, positions and orders come in later versions.
composer require baconfy/exchangesThe package requires only PSR interfaces — bring your own PSR-18/PSR-17 implementation:
composer require guzzlehttp/guzzleuse Baconfy\Exchanges\ExchangeManager;
use Baconfy\Exchanges\Enums\MarketType;
use Baconfy\Exchanges\Enums\Timeframe;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
$manager = new ExchangeManager(new Client(), new HttpFactory());
$candles = $manager->make('bybit', MarketType::Perpetual)->klines('BTCUSDT', Timeframe::M5, 100);
foreach ($candles as $candle) {
// oldest to newest, every OHLCV field is an exact Brick\Math\BigDecimal
echo "{$candle->openTime->format('c')}: {$candle->close}\n";
}Timeframe::M1, M5, M15, M30, H1, H4, D1, W1 — each driver translates these to its own dialect (e.g. Bybit's interval=240 for H4).
Every failure the package throws implements Baconfy\Exchanges\Exceptions\ExchangeException, so you can catch broadly or narrowly:
use Baconfy\Exchanges\Exceptions\ApiException;
use Baconfy\Exchanges\Exceptions\ExchangeException;
use Baconfy\Exchanges\Exceptions\TransportException;
try {
$manager->make('bybit', MarketType::Perpetual)->klines('BTCUSDT', Timeframe::M5);
} catch (ApiException $e) {
// the exchange responded with an error: $e->retCode / $e->retMsg
} catch (TransportException $e) {
// network failure, HTTP error status, or invalid JSON
} catch (ExchangeException $e) {
// any other package failure (unknown driver, wrong credentials type, ...)
}$manager->register('binance', BinanceDriver::class);
$manager->available(); // ['bybit', 'binance']Every driver constructor follows the same shape — (ClientInterface $http, RequestFactoryInterface $requests, MarketType $market, ?Credentials $credentials = null) — so ExchangeManager::make() can build any of them generically.
The package ships an optional ExchangesServiceProvider, auto-discovered via Composer. It registers ExchangeManager as a container singleton, wired to whatever PSR-18/PSR-17 implementations your app has bound — it does not bind Exchange::class itself, since each connection may use a different driver and credentials:
$manager = app(\Baconfy\Exchanges\ExchangeManager::class);
$exchange = $manager->make($connection->driver, MarketType::Perpetual, $credentials);illuminate/support is a suggest, never a hard dependency — the core works in a plain PHP script with no framework at all.
composer install
vendor/bin/pestNo test hits the network: HTTP is mocked at the PSR-18 boundary, and Bybit fixtures under tests/Fixtures/ are real API responses, versioned deliberately.
Design decisions, invariants and the full build rationale live in .claude/ARCHITECTURE.md — the source of truth for this package.