Decentralized, Self-Hosted Cryptocurrency Payments

Usage and Configuration Options for the Cryptopay Plugin

The Cryptopay plugin in conjunction with the Unchained Cryptopay Payment Gateway allows you to accept payments in Bitcoin, Ethereum and arbitrary ERC20 tokens without relying on centralized payment providers and without your private key. It consists of two parts:

  • A payment plugin that generates new addresses for every order and updates the payment status when the order is paid.
  • A price feed plugin that continuously gets the Chainlink price feeds to convert between the different currencies.

Because the plugin is using the currency rate system of Unchained with support for arbitrary rate plugins, it can also be used in conjunction with other price feeds (e.g., Coinbase) if desired.

Payment Plugin

Environment variables

You have to set CRYPTOPAY_SECRET, CRYPTOPAY_BTC_XPUB (if you want to accept Bitcoin payments), and CRYPTOPAY_ETH_XPUB (if you want to accept Ethereum payments):

NAMEDefault ValueDescription
CRYPTOPAY_SECRETShared secret for communication with the payment gateway. Has to be equal to unchained.secret in the payment gateway configuration (cryptopay.yaml).
CRYPTOPAY_WEBHOOK_PATH/payment/cryptopayThe path that is used for the payment webhook. Has to correspond to the path in unchained.transaction-webhook-url of the payment gateway configuration (cryptopay.yaml).
CRYPTOPAY_BTC_XPUBExtended Bitcoin public key.
CRYPTOPAY_BTC_TESTNETfalseDenotes whether the extended public key is a testnet public key.
CRYPTOPAY_ETH_XPUBExtended Ethereum public key.
CRYPTOPAY_MAX_RATE_AGE360Maximum age of an exchange rate (in seconds) such that it is still considered for the conversion.
CRYPTOPAY_MAX_CONV_DIFF0.01Configures how much lower the converted amount is allowed to be such that it is still considered as payed. 0.01 (1%) means that for a product with a price of $100, a converted amount of $99 would still be valid. Allows to incorporate the volatility of crypto exchange rates, but can also be set to 0.00 if always the full amount (or more) must be payed.

Ethereum Address Derivation

In contrast to Bitcoin, many Ethereum wallets do not expose the extended public key to you. However, it is very easy to generate a wallet and retrieve it with Python or JavaScript. In JavaScript when using ethers.js, the code to do so looks like this:

let HDNode = require('ethers').utils.HDNode;
let mnemonic = "<redacted>";
let masterNode = HDNode.fromMnemonic(mnemonic);
let hardenedMaster = masterNode.derivePath("m/44'/60'/0'");

// Extended public key (of hardened master node, i.e. path "m/44'/60'/0'")
let xpub = hardenedMaster.neuter().extendedKey;

Similarly, bip-utils can be used in Python:

import binascii
from bip_utils import Bip39SeedGenerator, Bip44Coins, Bip44

# Generate from mnemonic
mnemonic = "<redacted>"
seed_bytes = Bip39SeedGenerator(mnemonic).Generate()
# Or specify seed manually
# seed_bytes = binascii.unhexlify(b"<seed>")
# Derivation path returned: m
bip44_mst_ctx = Bip44.FromSeed(seed_bytes, Bip44Coins.ETHEREUM)
xpub = bip44_mst_ctx.Purpose().Coin().Account(0).PublicKey().ToExtended() # m/44'/60'/0'

Note that for security reasons, the extended public key should never be generated on a system that is publicly accessible. You should always do this offline and only reference the extended public key on publicly accessible systems. Then, an attacker cannot access your funds, even if your system is completely compromised.


The payment plugin supports products that have a crypto price (including ERC20 tokens, where the contractAddress is set on the currency), but also those that only have a fiat price. When the product has a crypto price, it is assumed that the price is recorded with 8 decimal places. For instance, the following UpdateProductCommercePricingInput would be used for a product that costs 1 ETH:

    amount: 10 ** 8,
    maxQuantity: 0,
    isTaxable: false,
    isNetPrice: false,
    currencyCode: 'ETH',
    countryCode: 'CH',

When a product only has a fiat price (e.g., USD), the paid crypto amount is converted to the fiat currency using Unchained's rate system. Because rates for cryptocurrencies can be very volatile, the behavior of the engine for the conversion is configurable. With CRYPTOPAY_MAX_RATE_AGE, you can configure the maximum age (in seconds) for the exchange rate such that it is still considered for the conversion. When using the Cryptopay pricing plugin (see below), the rates are updated every 10 seconds during normal operation. With the parameter CRYPTOPAY_MAX_CONV_DIFF, you can configure if an order should be considered as paid even if the converted amount is lower than the fiat price of the product. This can happen when the exchange rate changes between the generation of the address and the payment (including confirmation on the blockchain) of the user. A value of 0.00 means that the converted amount always has to be equal or higher than the configured fiat price of the product.

Deriving Addresses for an Order

To get the addresses that belong to the order and are displayed to the end user, the signPaymentProviderForCheckout mutation is used:

    orderPaymentId: "order payment id of the cart you want to checkout"

To get the order payment id of the current active cart of the logged in user you can

me {
    cart {
        payment {

The mutation returns a stringified JSON-object with the different addresses:

  "data": {
    "signPaymentProviderForCheckout": "[{\"currency\":\"BTC\",\"address\":\"mkFQhpfDfW9tqybJA47b71Wxq3XKV2DSwT\"},{\"currency\":\"ETH\",\"address\":\"0xaBC2bCA51709b8615147352C62420F547a63A00c\"}]"

Note that only addresses are returned if the corresponding extended public key is set. If you only set CRYPTOPAY_ETH_XPUB, the array will therefore only contain an Ethereum address.

The plugin ensures that when calling the mutation multiple times, the returned addresses are always identical for a given orderPaymentId. When integrating it into the frontend, the addresses can therefore be shown in multiple places (e.g., at the checkout stage and inside the orders overview in the account page to allow deferred payments with crypto) and always be retrieved by calling the mutation.

Accepting Payments in ERC20 tokens

If you want to accept payments in a token that follows the ERC20 standard, you have to create a corresponding currency and provide a contractAddress, e.g.:

mutation createMATIC {
  createCurrency(currency: {isoCode: "MATIC", 
                            contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0"}) {

You can then set prices for products in this currency and use it for payments that are converted to fiat (with arbitrary price feeds, e.g. the decentralized Cryptopay feeds based on Chainlink). Note that for security reasons, payments are only accepted for the ERC20 tokens that you have added to your store as a currency.

Pricing Plugin

The Cryptopay pricing plugin provides rates for the Unchained rate system and can therefore be used by the payment plugin for the rate conversions. The gateway sends the current rate for BTC, ETH, and the configured ERC20 tokens to the plugin every 10 seconds. If no Chainlink price feed exists for a given currency pair (e.g., if you have an online shop that only has CHF prices and want to accept payments in MATIC, but there is no direct MATIC / CHF feed), the gateway tries to use USD as an intermediate currency in the calculation (e.g., the MATIC / USD and CHF / USD feeds are used in the previous example to calculate the MATIC / CHF rate). This happens automatically in the background and you do not have to worry about it.

Environment variables

NAMEDefault ValueDescription
CRYPTOPAY_SECRETShared secret for communication with the payment gateway. Has to be equal to unchained.secret in the payment gateway configuration (cryptopay.yaml).
CRYPTOPAY_PRICING_WEBHOOK_PATH/pricing/cryptopayThe path that is used for the pricing webhook. Has to correspond to the path in unchained.price-webhook-url of the payment gateway configuration (cryptopay.yaml).
Edit on GitHub