```
Create a Yii2 component CoinbaseComponent.php

This component will handle the API key and Guzzle client setup, and create charges.

<?php
namespace app\components;

use GuzzleHttp\Client;
use Yii;

class CoinbaseComponent extends \yii\base\Component
{
    public string $apiKey;
    public string $apiVersion = '2018-03-22';

    /** @var Client */
    private Client $_client;

    public function init()
    {
        parent::init();

        if (!$this->apiKey) {
            throw new \RuntimeException('Coinbase API key must be set');
        }

        $this->_client = new Client([
            'base_uri' => 'https://api.commerce.coinbase.com/',
            'headers' => [
                'X-CC-Api-Key' => $this->apiKey,
                'X-CC-Version' => $this->apiVersion,
                'Content-Type' => 'application/json',
            ],
        ]);
    }

    /**
     * Create a coinbase charge.
     *
     * @param array $params Array with keys: name, description, amount, currency, metadata (optional)
     * @return array Decoded JSON response from Coinbase Commerce with charge data
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    public function createCharge(array $params): array
    {
        $payload = [
            'name' => $params['name'] ?? 'Order',
            'description' => $params['description'] ?? '',
            'local_price' => [
                'amount' => $params['amount'],
                'currency' => strtoupper($params['currency']),
            ],
            'pricing_type' => 'fixed_price',
        ];

        if (!empty($params['metadata']) && is_array($params['metadata'])) {
            $payload['metadata'] = $params['metadata'];
        }

        $response = $this->_client->post('charges', [
            'json' => $payload,
        ]);

        $data = json_decode($response->getBody()->getContents(), true);

        if (!isset($data['data'])) {
            throw new \RuntimeException('Invalid response from Coinbase Commerce API');
        }

        return $data['data'];
    }
}

Add this component to your application config (e.g., config/web.php):

'components' => [
    'coinbase' => [
        'class' => 'app\components\CoinbaseComponent',
        'apiKey' => getenv('COINBASE_COMMERCE_API_KEY'), // or your api key directly here (not recommended)
    ],
    // other components ...
],

Step 2: Create CoinbaseController.php

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use yii\web\Response;

class CoinbaseController extends Controller
{
    // Disable CSRF validation for webhook endpoint since Coinbase posts here
    public $enableCsrfValidation = false;

    /**
     * Action to show a page with a Coinbase Commerce JS modal payment button.
     *
     * Example usage: /coinbase/payment?orderId=123
     */
    public function actionPayment()
    {
        $orderId = Yii::$app->request->get('orderId', null);
        if (!$orderId) {
            return $this->asJson(['error' => 'No order ID provided'])->setStatusCode(400);
        }

        // TODO: Load your order data from DB, calculate amount
        // For example purposes, we hardcode:
        $orderName = "Order #$orderId";
        $orderDescription = "Payment for order #$orderId";
        $amount = "49.99"; // in USD
        $currency = "USD";

        // Create a new Coinbase Commerce charge
        try {
            $charge = Yii::$app->coinbase->createCharge([
                'name' => $orderName,
                'description' => $orderDescription,
                'amount' => $amount,
                'currency' => $currency,
                'metadata' => [
                    'order_id' => $orderId,
                ],
            ]);
        } catch (\Exception $e) {
            Yii::error("Coinbase createCharge failed: " . $e->getMessage());
            return $this->asJson(['error' => 'Unable to create payment charge'])->setStatusCode(500);
        }

        // Pass the charge code (ID) to the view for the JS modal checkout button
        return $this->render('payment', [
            'chargeCode' => $charge['code'],
            'orderId' => $orderId,
            'amount' => $amount,
            'currency' => $currency,
        ]);
    }

    /**
     * Webhook endpoint to receive payment notifications from Coinbase Commerce.
     * URL example: /coinbase/webhook
     */
    public function actionWebhook()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;

        $request = Yii::$app->request;
        $rawBody = $request->rawBody;
        $sigHeader = $request->headers->get('X-CC-Webhook-Signature');
        $webhookSecret = getenv('COINBASE_COMMERCE_WEBHOOK_SECRET');

        if (!$this->verifySignature($rawBody, $sigHeader, $webhookSecret)) {
            Yii::error('Invalid Coinbase webhook signature');
            return ['error' => 'Invalid signature'];
        }

        $payload = json_decode($rawBody, true);
        if (!$payload) {
            Yii::error('Invalid webhook payload');
            return ['error' => 'Invalid payload'];
        }

        $eventType = $payload['event']['type'] ?? null;
        $chargeData = $payload['event']['data'] ?? [];

        Yii::info("Coinbase webhook event received: $eventType");

        // Example handling of charge:confirmed event
        if ($eventType === 'charge:confirmed') {
            $metadata = $chargeData['metadata'] ?? [];
            $orderId = $metadata['order_id'] ?? null;
            $chargeCode = $chargeData['code'] ?? null;

            if ($orderId) {
                // TODO: update order in your DB as paid
                Yii::info("Order $orderId marked as paid (charge code $chargeCode).");
            }
        }

        return ['status' => 'success'];
    }

    /**
     * Verify Coinbase webhook signature.
     *
     * @param string $payload Raw POST request body
     * @param string $signature The X-CC-Webhook-Signature header
     * @param string $secret Your webhook shared secret
     * @return bool
     */
    protected function verifySignature(string $payload, ?string $signature, string $secret): bool
    {
        if (!$signature || !$secret) {
            return false;
        }
        $computed = hash_hmac('sha256', $payload, $secret);
        return hash_equals($computed, $signature);
    }
}

Step 3: Create view file views/coinbase/payment.php

<?php
/** @var string $chargeCode */
/** @var string $orderId */
/** @var string $amount */
/** @var string $currency */
?>

<h1>Pay Order #<?= htmlspecialchars($orderId) ?></h1>
<p>Amount: <?= htmlspecialchars($amount) ?> <?= htmlspecialchars($currency) ?></p>

<button
  class="coinbase-commerce-button"
  data-code-id="<?= htmlspecialchars($chargeCode) ?>"
  data-button-style="custom_large"
>
  Pay with Crypto
</button>

<script src="https://commerce.coinbase.com/v1/checkout.js"></script>

Step 4: Environment variables

Set your Coinbase Commerce API key and webhook secret in your .env or server environment:

COINBASE_COMMERCE_API_KEY=your_api_key_here
COINBASE_COMMERCE_WEBHOOK_SECRET=your_webhook_shared_secret_here

How it works:

    You visit /coinbase/payment?orderId=123
    Controller creates a charge via Coinbase Commerce API.
    The payment page renders a button with the JS modal checkout.
    User clicks the button, Coinbase modal popup opens — user pays.
    Coinbase sends webhook to /coinbase/webhook on your server.
    Your webhook validates signature, reads event, updates order status.

Notes:

    Replace the TODO comments with your real order loading/updating logic.
    You can customize button style or use multiple coins as per Coinbase docs.
    Ensure your webhook URL is HTTPS and publicly accessible.
    Test everything in Coinbase test mode before going live.

If you want, I can help you further with:

    Adding order DB integration example
    Handling multiple event types
    Frontend UX improvements

Just say the word!

<?php
namespace app\components;

use GuzzleHttp\Client;
use Yii;

class CoinbaseComponent extends \yii\base\Component
{
    public string $apiKey;
    public string $apiVersion = '2018-03-22';

    /** @var Client */
    private Client $_client;

    public function init()
    {
        parent::init();

        if (!$this->apiKey) {
            throw new \RuntimeException('Coinbase API key must be set');
        }

        $this->_client = new Client([
            'base_uri' => 'https://api.commerce.coinbase.com/',
            'headers' => [
                'X-CC-Api-Key' => $this->apiKey,
                'X-CC-Version' => $this->apiVersion,
                'Content-Type' => 'application/json',
            ],
        ]);
    }

    /**
     * Create a coinbase charge.
     *
     * @param array $params Array with keys: name, description, amount, currency, metadata (optional)
     * @return array Decoded JSON response from Coinbase Commerce with charge data
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    public function createCharge(array $params): array
    {
        $payload = [
            'name' => $params['name'] ?? 'Order',
            'description' => $params['description'] ?? '',
            'local_price' => [
                'amount' => $params['amount'],
                'currency' => strtoupper($params['currency']),
            ],
            'pricing_type' => 'fixed_price',
        ];

        if (!empty($params['metadata']) && is_array($params['metadata'])) {
            $payload['metadata'] = $params['metadata'];
        }

        $response = $this->_client->post('charges', [
            'json' => $payload,
        ]);

        $data = json_decode($response->getBody()->getContents(), true);

        if (!isset($data['data'])) {
            throw new \RuntimeException('Invalid response from Coinbase Commerce API');
        }

        return $data['data'];
    }
}

I would prefer their was two files, one that handles the component where the keys are within the config of yii2 and setup and has getters for the conbase class that handles the api setup, so create and anything else.

Absolutely! We can split this into two classes/files for better separation of concerns and cleaner architecture:

    Component class: Handles integration with Yii2, loads config (API key, version, webhook secret), and provides an interface to get the API handler.

    API handler class: Contains all logic related to Coinbase Commerce API calls (create charge, list charges, etc).

Here's a clean separation approach:
File 1: components/CoinbaseComponent.php

    Yii2 component
    Reads configuration from Yii application config or environment
    Instantiates and returns an instance of the API handler class

<?php
namespace app\components;

use Yii;

class CoinbaseComponent extends \yii\base\Component
{
    public string $apiKey = '';
    public string $apiVersion = '2018-03-22';
    public string $webhookSecret = '';

    private ?CoinbaseApi $client = null;

    public function init()
    {
        parent::init();

        if (empty($this->apiKey)) {
            throw new \RuntimeException('Coinbase API key must be configured.');
        }
        if (empty($this->webhookSecret)) {
            Yii::warning('Coinbase webhook secret is not configured. Webhook verification will fail.');
        }
    }

    /**
     * Get the Coinbase API client instance
     *
     * @return CoinbaseApi
     */
    public function getClient(): CoinbaseApi
    {
        if ($this->client === null) {
            $this->client = new CoinbaseApi($this->apiKey, $this->apiVersion);
        }
        return $this->client;
    }
}

File 2: components/CoinbaseApi.php

    This class handles direct Coinbase Commerce API calls using Guzzle.
    Contract: methods like createCharge(), getCharge(), etc.

<?php
namespace app\components;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;

class CoinbaseApi
{
    private string $apiKey;
    private string $apiVersion;
    private Client $client;

    public function __construct(string $apiKey, string $apiVersion = '2018-03-22')
    {
        $this->apiKey = $apiKey;
        $this->apiVersion = $apiVersion;

        $this->client = new Client([
            'base_uri' => 'https://api.commerce.coinbase.com/',
            'headers' => [
                'X-CC-Api-Key' => $this->apiKey,
                'X-CC-Version' => $this->apiVersion,
                'Content-Type' => 'application/json',
            ],
        ]);
    }

    /**
     * Create a charge on Coinbase Commerce.
     *
     * @param array $params ['name', 'description', 'amount', 'currency', 'metadata' => []]
     * @return array Coinbase charge data
     * @throws GuzzleException
     * @throws \RuntimeException
     */
    public function createCharge(array $params): array
    {
        $payload = [
            'name' => $params['name'] ?? 'Order',
            'description' => $params['description'] ?? '',
            'local_price' => [
                'amount' => $params['amount'],
                'currency' => strtoupper($params['currency']),
            ],
            'pricing_type' => 'fixed_price',
        ];

        if (!empty($params['metadata']) && is_array($params['metadata'])) {
            $payload['metadata'] = $params['metadata'];
        }

        $response = $this->client->post('charges', [
            'json' => $payload,
        ]);

        $data = json_decode($response->getBody()->getContents(), true);

        if (!isset($data['data'])) {
            throw new \RuntimeException('Invalid response from Coinbase Commerce API');
        }

        return $data['data'];
    }

    /**
     * Get details of a charge by ID or code.
     *
     * @param string $chargeId
     * @return array
     * @throws GuzzleException
     */
    public function getCharge(string $chargeId): array
    {
        $response = $this->client->get("charges/{$chargeId}");

        $data = json_decode($response->getBody()->getContents(), true);

        if (!isset($data['data'])) {
            throw new \RuntimeException('Invalid response from Coinbase Commerce API');
        }

        return $data['data'];
    }
}

How To Configure It

In your Yii2 config file (e.g., config/web.php):

'components' => [
    'coinbase' => [
        'class' => 'app\components\CoinbaseComponent',
        'apiKey' => getenv('COINBASE_COMMERCE_API_KEY'),
        'webhookSecret' => getenv('COINBASE_COMMERCE_WEBHOOK_SECRET'),
        // Optional override
        'apiVersion' => '2018-03-22',
    ],
    // other components...
],

How To Use It in Your Controller

$chargeData = Yii::$app->coinbase->client->createCharge([
    'name' => 'Test order',
    'description' => 'Test description',
    'amount' => '10.00',
    'currency' => 'USD',
    'metadata' => ['order_id' => 123],
]);

Benefits:

    Separation of concerns: CoinbaseComponent is your Yii integration and config holder, API logic stays in CoinbaseApi.
    Easier unit testing for API layer.
    You get easy access to $this->client anywhere via Yii::$app->coinbase->client.
    Allows you to extend CoinbaseApi with more endpoints later on.

If you'd like, I can help you build:

    Example controller for payment creation & webhook with this refactor
    A Yii2 widget for rendering the Coinbase Commerce button modal

Just ask!

so on this subject, im trying to understand how to price things, i have thing in usd.. and I dont know how to price things for bitcoin.. Im using Money PHP and madea yii2 helper using phpmoney to handle currency.

Great question! When pricing products or services in USD but accepting Bitcoin (or any cryptocurrency) payments via Coinbase Commerce, the best practice is to keep your prices in fiat (e.g., USD) and use Coinbase Commerce’s fixed price checkout feature which does the crypto conversion for you in real-time at payment.

Let me explain in detail and how to incorporate with your Yii2 + MoneyPHP setup.
How Coinbase Commerce Pricing Works

    You create a charge specifying:
        The price amount in a fiat currency (e.g., USD) under the local_price key.
        Use pricing_type set to "fixed_price".
    Coinbase Commerce converts this fiat amount to crypto currencies (BTC, ETH etc) on the fly at checkout using current exchange rates.
    The buyer sees the equivalent crypto amount to pay — you don’t have to do the conversion yourself.
    This means your backend only needs to work with your base fiat amount.

Why This Is Good

    Keeps your accounting simple (everything defined in USD).
    No need to fetch live crypto prices or worry about volatility while user is checking out.
    Coinbase Commerce handles payment confirmation regardless of crypto volatility.
    Your backend gets notified via webhooks once payment is confirmed regardless of the particular crypto used.

How to Integrate With MoneyPHP / Yii2

Assuming:

    You handle all your prices via the moneyphp/money library.
    Your prices are stored internally in USD cents (or smallest unit).
    You generate Coinbase Commerce charges passing prices as decimal strings.

Example flow:

use Money\Money;
use Money\Currency;

// Let's say your product price is $10.25 USD
$price = new Money(1025, new Currency('USD'));

// Convert to a string with decimal point that Coinbase Commerce expects
// (Coinbase expects strings like "10.25", so divide cents by 100)
$amount = number_format($price->getAmount() / 100, 2, '.', ''); // "10.25"

// Now pass to CoinbaseCommerce create charge:
$chargeData = Yii::$app->coinbase->client->createCharge([
    'name' => 'Awesome Product',
    'description' => 'Great stuff',
    'amount' => $amount,
    'currency' => 'USD',
    // optionally metadata, etc.
]);

Important:

    Do NOT convert your USD prices to BTC yourself and pass that as the amount.
    Always send your amount with a fiat currency like USD or EUR.
    Coinbase Commerce handles the crypto conversion, so your users pay the right equivalent in BTC or whichever crypto they choose.
    Your webhook calls will confirm payments regardless of what crypto was used.

What if you want to show Bitcoin price on your site?

    You can fetch BTC to USD price from popular APIs like CoinGecko, Coinbase Pro, etc.
    Convert your USD price to BTC using that rate to show approximate BTC amount to users.
    But don't trust this for creating charges or accepting payment — only for display purposes.
    Always create charges with USD and let Coinbase handle the conversion.

Additional Tips

    Watch out for rounding and decimal precision. Coinbase expects string amounts with up to 2 decimal places for fiat.
    If you want multi-currency pricing, MoneyPHP works well with currency conversion modules or your own exchange rate feeds — but for Coinbase Commerce, always send the local_price in fiat.
    On webhook payment confirmation, record the exact paid crypto currency and amount sent by Coinbase for your records.

Summary
Step 	Details
Price storage 	Store price in USD (e.g., MoneyPHP)
Charge creation 	Pass USD amount as decimal string
Coinbase performs 	Crypto conversion & checkout amount
Your webhook 	Confirms payment with actual crypto

If you want, I can help you implement a Yii2 helper/service to:

    Create charge from MoneyPHP price
    Fetch and display approximate BTC price on product pages (for UX)
    Handle payment webhooks and update your DB accordingly

Just let me know!

I have this as my php money helper

declare(strict_types=1);

namespace app\helpers;

use Throwable;

use Money\Money;
use Money\Currency;
use Money\Number;
use Money\Teller;
use Money\Currencies\ISOCurrencies;
use Money\Parser\IntlLocalizedDecimalParser;
use Money\Formatter\IntlMoneyFormatter;
//use Money\Formatter\DecimalMoneyFormatter;

use Yii;
use yii\base\InvalidConfigException as YiiBaseInvalidConfigException;

use app\helpers\ExceptionLogger;
use app\helpers\FormatterHelper;
use app\helpers\LogHelper;

class MoneyHelper
{
    private static ?FormatterHelper $_formatter = null;
    
    // BTC currency code
    private const CURRENCY_BTC = 'BTC';
    
    // Bitcoin decimals/subunits (satoshis)
    private const BTC_DECIMALS = 8;
    
    /**
    * Lazy-initialize and return the FormatterHelper instance.
    * Ensures the formatter is created once and reused across static methods.
    * @return FormatterHelper
    */
    private static function getFormatter(): FormatterHelper
    {
        if (self::$_formatter === null)
        {
            self::$_formatter = FormatterHelper::create(\Yii::$app->formatter);
        }
        
        return self::$_formatter;
    }
    
    /**
    * Validates that the provided value is numeric and logs an error if it is not.
    * @param mixed $value The value to be checked.
    * @param string $context A context string for the error message.
    * @return void
    * @throws \yii\base\InvalidConfigException
    */
    private static function validateNumeric($value, string $context): void
    {
        if (!is_numeric($value))
        {
            $error = is_object($value) ? get_class($value) : gettype($value);
            $msg = \Yii::t('app', 'Money value has to be a numeric value in {context}, `{error}`', [
                'context' => $context,
                'error' => $error,
            ]);
            
            \Yii::error($msg);
            
            throw new YiiBaseInvalidConfigException($msg);
        }
    }
    
    public static function getDiscount(string $price, string $discount): string
    {
        try
        {
            self::validateNumeric($price, 'getDiscount');
            self::validateNumeric($discount, 'getDiscount');
            
            $teller = Teller::USD();
            $discountAmount = $teller->multiply($price, $discount);
            
            return (string) $discountAmount;
        }
        catch (Throwable $e)
        {
            // Use the ExceptionLogger to handle and log the exception
            ExceptionLogger::log($e, 'money_helper_getDiscount_error_');
            
            return $e->getMessage();
        }	
    }
    
    public static function totalAmount(int|string $quantity, int|string $amount): int|string
    {
        try
        {
            self::validateNumeric($quantity, 'totalAmount');
            self::validateNumeric($amount, 'totalAmount');
            
            $teller = Teller::CAD();
            
            return (int) $teller->multiply($quantity, $amount);
        }
        catch (Throwable $e)
        {
            // Use the ExceptionLogger to handle and log the exception
            ExceptionLogger::log($e, 'money_helper_totalAmount_error_');
            
            return $e->getMessage();
        }
    }
    
    public static function getStripeFee(int|string $amount): string
    {
        try
        {
            self::validateNumeric($amount, 'getStripeFee');
            
            $formatter = self::getFormatter();
            
            $moneyFormatter = new Money($amount, new Currency($formatter->currencyCode()));
            $fees = $moneyFormatter->multiply('1.062', Money::ROUND_UP);
            
            return (string) $fees->getAmount();
        }
        catch (Throwable $e)
        {
            // Use the ExceptionLogger to handle and log the exception
            ExceptionLogger::log($e, 'money_helper_getStripeFee_error_');
            
            return $e->getMessage();
        }
    }
    
    public static function getTax(int|string $amount): string
    {
        try
        {
            self::validateNumeric($amount, 'getTax');
            
            $formatter = self::getFormatter();
            
            $moneyFormatter = new Money($amount, new Currency($formatter->currencyCode()));
            $gross = $moneyFormatter->multiply('1.13', Money::ROUND_UP);
            
            return (string) $gross->getAmount();
        }
        catch (Throwable $e)
        {
            // Use the ExceptionLogger to handle and log the exception
            ExceptionLogger::log($e, 'money_helper_getTax_error_');
            
            return $e->getMessage();
        }
    }
    
    public static function getIntl(int|string $amount): string
    {
        try
        {
            self::validateNumeric($amount, 'getIntl');
            
            $formatter = self::getFormatter();
            
            $money = new Money($amount, new Currency($formatter->currencyCode()));
            $currencies = new ISOCurrencies();
            $numberFormatter = new \NumberFormatter($formatter->locale(), \NumberFormatter::CURRENCY);
            $moneyFormatter = new IntlMoneyFormatter($numberFormatter, $currencies);
            
            return (string) $moneyFormatter->format($money);
        }
        catch (Throwable $e)
        {
            // Use the ExceptionLogger to handle and log the exception
            ExceptionLogger::log($e, 'money_helper_getIntl_error_');
            
            return $e->getMessage();
        }
    }
    
    public static function getMoneyValue(int $value): string
    {
        LogHelper::debug("getMoneyValue received: $value");
    
        try
        {
            self::validateNumeric($value, 'getMoneyValue');
            
            // base10(2) shifts decimal 2 places to the left: e.g. "100022" → "1000.22"
            $number = Number::fromNumber($value)->base10(2);
            
            return number_format((float)(string) $number, 2, '.', '');
        }
        catch (Throwable $e)
        {
            // Use the ExceptionLogger to handle and log the exception
            ExceptionLogger::log($e, 'money_helper_getMoneyValue_error_');
            
            return $e->getMessage();
        }
    }
    
    public static function parseTotal(string $total): int
    {
        try
        {
            LogHelper::debug("parseTotal received: {$total}");
            
            self::validateNumeric($total, 'parseTotal');
            
            $formatter = self::getFormatter();
            
            $currencies = new ISOCurrencies();
            $numberFormatter = new \NumberFormatter($formatter->locale(), \NumberFormatter::DECIMAL);
            $moneyParser = new IntlLocalizedDecimalParser($numberFormatter, $currencies);
            $money = $moneyParser->parse($total, new Currency($formatter->currencyCode()));
            
            if ($money === null)
            {
                throw new YiiBaseInvalidArgumentException("Failed to parse money amount from '{$total}'");
            }
            
            LogHelper::debug("parseTotal returning cents: " . $money->getAmount());
            
            return (int) $money->getAmount();
        }
        catch (Throwable $e)
        {
            // Use the ExceptionLogger to handle and log the exception
            ExceptionLogger::log($e, 'money_helper_parseTotal_error_');
            
            // Let caller handle exception, don't return string error
            throw $e;
        }
    }
    
    public static function debugCurrencySubunit(): void
    {
        $currencies = new ISOCurrencies();
        $currency = new Currency(self::getFormatter()->currencyCode());
        $subunit = $currencies->subunitFor($currency);
        
        LogHelper::debug("Currency subunit for {$currency->getCode()} is {$subunit}");
    }
    
    public static function btcToSatoshi(float|string $btcAmount): int
    {
        self::validateNumeric($btcAmount, 'btcToSatoshi');
        
        // Multiply by 10^8 and round to int
        return (int) round((float) $btcAmount * (10 ** self::BTC_DECIMALS));
    }
    
    public static function satoshiToBtc(int $satoshiAmount): string
    {
        // Divide by 10^8
        return number_format($satoshiAmount / (10 ** self::BTC_DECIMALS), self::BTC_DECIMALS, '.', '');
    }
    
    public static function parseBtc(string $btc): int
    {
        self::validateNumeric($btc, 'parseBtc');
        
        return self::btcToSatoshi((float) $btc);
    }
    
    public static function formatBtc(int $satoshiAmount): string
    {
        return self::satoshiToBtc($satoshiAmount);
    }
    
    public static function formatBtcIntl(int $satoshiAmount): string
    {
        try
        {
            $btcString = self::satoshiToBtc($satoshiAmount);
            
            $formatter = self::getFormatter();
            $numberFormatter = new \NumberFormatter($formatter->locale(), \NumberFormatter::DECIMAL);
            $formatted = $numberFormatter->format((float) $btcString);
            
            return $formatted . ' BTC';
        }
        catch (Throwable $e)
        {
            // Use the ExceptionLogger to handle and log the exception
            ExceptionLogger::log($e, 'money_helper_formatBtcIntl_error_');
            
            return self::formatBtc($satoshiAmount);
        }
    }
}

I dont know if the bitcoin is setup properly, any suggestions welcomed.

Your helper shows a thorough, well-structured approach to handling money, both fiat and BTC, with good use of MoneyPHP and internationalization. Your BTC handling reflects common practices (8 decimals for satoshis, conversions, formatting). That said, I'll provide targeted feedback and suggestions on your Bitcoin-related methods and general improvements to consolidate your efforts.
1. BTC Handling Looks Solid

    You correctly define BTC decimal places as 8 (self::BTC_DECIMALS).
    btcToSatoshi() multiplies BTC amount by 10^8 and rounds.
    satoshiToBtc() divides satoshis by 10^8 and formats with 8 decimals.
    Use of validateNumeric() protects methods from invalid input.
    Intl formatting via NumberFormatter in formatBtcIntl() is a nice UX touch.

These are standard approaches and should work well.
2. Potential Improvements & Checks
a) btcToSatoshi() rounding

    You round after multiplying, which is correct.
    As BTC amounts may be provided as float or string, be mindful that floats can have binary precision errors.
    If precision is critical, consider using arbitrary precision math (e.g., GMP or BCMath), or use string-based math libs like brick/money.
    Example with BCMath for precision:

public static function btcToSatoshi(string|float $btcAmount): int
{
    self::validateNumeric($btcAmount, 'btcToSatoshi');
    
    $btcStr = is_float($btcAmount) ? number_format($btcAmount, self::BTC_DECIMALS, '.', '') : (string)$btcAmount;
    
    // Multiply using BCMath to avoid float precision issues
    $satoshiStr = bcmul($btcStr, bcpow('10', (string)self::BTC_DECIMALS), 0); // scale 0 = integer result
    
    return (int) $satoshiStr;
}

This avoids floating point inaccuracies, especially with fractional BTC amounts.
b) satoshiToBtc() using number_format

    Using number_format directly is mostly fine, but note it returns string with trailing zeros.
    If you want to suppress trailing zeros (e.g., 0.10000000 → 0.1), consider:

public static function satoshiToBtc(int $satoshiAmount, bool $trimTrailingZeroes = false): string
{
    $btc = bcdiv((string) $satoshiAmount, bcpow('10', (string)self::BTC_DECIMALS, 0), self::BTC_DECIMALS);
    if ($trimTrailingZeroes) {
        $btc = rtrim(rtrim($btc, '0'), '.');
    }
    return $btc;
}

Usually, keeping fixed 8 decimals can be better for consistent display.
c) Naming clarification

    Consider naming these conversion methods clearly for your team e.g. btcToSatoshiInt() and satoshiIntToBtcString() to emphasize the satoshi as int and BTC as string conversion.

d) Use Money\Currency for BTC amount if needed

You rely mostly on primitives for BTC—if you want, you can create Money objects with BTC currency code, but note that MoneyPHP out of the box doesn't handle 8 decimal currencies (does 2 decimals by default). You may need to adjust currency subunits for BTC if using Money objects for BTC.
3. Fiat and BTC usage in your app & Coinbase Commerce

    Important: Coinbase Commerce expects fiat amount as decimal string (USD, EUR, etc) when creating a charge (see previous answer).
    Your helper can be used to handle fiat amounts properly.
    BTC conversion/formatting is mostly useful for displaying or calculating estimated BTC amounts on frontend, not for creating the actual charge.
    You do not need to convert your USD price manually to BTC to create Coinbase Commerce payment. Let Coinbase handle the conversion.

4. Example usage with your methods

use app\helpers\MoneyHelper;
use Money\Money;
use Money\Currency;

$priceCents = 1025;   // $10.25 in cents USD
$priceStr = MoneyHelper::getMoneyValue($priceCents); // "10.25" for Coinbase

// Create order, pass $priceStr to CoinbaseCommerce API as amount in USD

// If you want to display BTC approximate:
$btcApprox = '0.00023456'; // example rate from some API
$btcSats = MoneyHelper::btcToSatoshi($btcApprox);
echo MoneyHelper::formatBtcIntl($btcSats); // nicely formatted BTC string

Summary

    Your BTC helper methods are very good for most usages.
    Consider BCMath for precise conversion.
    Keep Coinbase Commerce fixed price in fiat currency string.
    Use your BTC functions mainly for display/conversion or internal purposes.
    Make sure any money objects you create for BTC account for 8 decimals if used in calculations—MoneyPHP defaults to 2 decimals, so may need extensions if you rely on Money objects for BTC.

If you want, I can help write some unit tests for your BTC helpers or a utility function to fetch/display real-time BTC/USD prices integrated with your helper, just ask!
```

yes please