Home How I accept bitcoin payments
Post
Cancel

How I accept bitcoin payments

Bitcoin is easy, lightning is hard.

I run my own bitcoin node but use a hosted bitcoin service provider for my lightning infrastructure.

You will learn how to semi-self-sovereignly accept bitcoin payments using BTCPay Server. And have your own lightning addresses (like an email address, but for your Bitcoin) with your own domain.

Public-facing stuff I usually host on a VPS at hetzner, personal stuff I host in my homelab. I usually do not want publicly accessible services to be dependent on my homelab because I actually use my homelab as a lab, stuff is changing all the time. But for a full-node you currently need round about 750 GB of disk space which is expensive in the cloud. And for my use-case it is not important for the node to be always up and running. I do not yet have a use-case where I need to verify incoming payments. You can always receive BTC on chain by just providing a static address. And for small amounts lightning is recommended anyway.

hosting

On my homeserver I use NixOS. Following is an abbrevated version of my /etc/nixos/configuration.nix. If you are familiar with containers (and I suppose you are when you are reading this) it should be no problem to translate this to a corresponding run-command or compose-file. Keep in mind it can take weeks for your node to fully sync.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
{ config, pkgs, ... }:

{
  systemd.tmpfiles.rules = [
    "d /mnt/data/bitcoincore 0777 simon users -"
    "d /mnt/data/nbxplorer 0777 simon users -"
    "d /mnt/data/nbxplorer-db 0777 simon users -"
    "d /mnt/data/nbxplorer-tailscale 0777 simon users -"
  ];

  systemd.services.create-bitcoin-network = with config.virtualisation.oci-containers; {
    serviceConfig.Type = "oneshot";
    script = ''
      ${pkgs.podman}/bin/podman network exists bitcoin || \
      ${pkgs.podman}/bin/podman network create bitcoin
      '';
  };

  virtualisation.oci-containers.containers = {

    bitcoincore = {
      image = "bitcoin/bitcoin";
      autoStart = true;
      extraOptions = [ "--network=bitcoin" ];
      volumes = [
        "/mnt/data/bitcoincore:/bitcoin/.bitcoin/"
      ];
      cmd = [
        "-listen=1" 
        "-server=1"
        "-txindex=1"
        "-rpcbind=0.0.0.0"
        "-rpcport=8332"
        "-rpcallowip=0.0.0.0/0"
        "-rpcuser=bitcoin"
        "-rpcpassword=bitcoin"
        "-disablewallet=1"
        "-whitelist=nbxplorer"
        "-zmqpubrawtx=tcp://0.0.0.0:28332"
        "-zmqpubrawblock=tcp://0.0.0.0:28332"
        "-zmqpubhashblock=tcp://0.0.0.0:28332"
        "-zmqpubhashtx=tcp://0.0.0.0:28332"
      ];
      environment = {
        BITCOIN_DATA = "/bitcoin/.bitcoin";
      };
    };

    nbxplorer = {
      image = "docker.io/nicolasdorier/nbxplorer:2.5.28";
      autoStart = true;
      autoRemoveOnStop = false;
      extraOptions = [ 
        "--network=container:nbxplorer-tailscale"
      ];
      environment = {
        NBXPLORER_NOAUTH = "1";
        NBXPLORER_CHAINS = "btc";
        NBXPLORER_NETWORK = "mainnet";
        NBXPLORER_SIGNALFILEDIR = "/datadir";
        NBXPLORER_BIND = "0.0.0.0:32838";
        NBXPLORER_BTCRPCURL = "http://bitcoincore:8332";
        NBXPLORER_BTCNODEENDPOINT = "bitcoincore:8333";
        NBXPLORER_BTCRPCUSER = "bitcoin";
        NBXPLORER_BTCRPCPASSWORD = "bitcoin";
        NBXPLORER_POSTGRES = "Username=nbxplorer;password=nbxplorer;Host=nbxplorer-db;Port=5432;Database=nbxplorer";
      };
      volumes = [
        "/mnt/data/nbxplorer:/datadir"
        "/mnt/data/bitcoincore:/root/.bitcoin:ro"
      ];
    };

    nbxplorer-db = {
      image = "postgres";
      autoStart = true;
      extraOptions = [ "--network=bitcoin" ];
      environment = {
        POSTGRES_PASSWORD = "nbxplorer";
        POSTGRES_USER = "nbxplorer";
        POSTGRES_DB = "nbxplorer";
      };
      volumes = [
        "/mnt/data/nbxplorer-db:/var/lib/postgresql/data"
      ];
    };

    nbxplorer-tailscale = {
      image = "docker.io/tailscale/tailscale:latest";
      autoStart = true;
      extraOptions = [ "--network=bitcoin" ]; 
      autoRemoveOnStop = false;
      hostname = "nbxplorer";
      capabilities = {
        net_admin = true;
        sys_module = true;
      };
      volumes = [
        "/mnt/data/nbxplorer-tailscale:/var/lib/tailscale"
        "/dev/net/tun:/dev/net/tun"
        "/etc/nixos/dozzle:/config"
      ];
      environment = {
        TS_AUTHKEY = "tskey-auth-asdf";
        TS_STATE_DIR = "/var/lib/tailscale";
      };
    };
  };
}

BTCPay connects to a bitcoin node not directly but through nbxplorer which I add with the tailscale-sidecar container to my tailnet.

My VPS is a Ubuntu instance running the publicly accessible BTCPay.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
services:
  btcpay:
    image: docker.io/btcpayserver/btcpayserver:2.1.6
    restart: unless-stopped
    environment:
      BTCPAY_BIND: "0.0.0.0:23000"
      BTCPAY_POSTGRES: "Username=btcpay;password=btcpay;Host=btcpay-db;Port=5432;Database=btcpay"
      BTCPAY_NETWORK: "mainnet"
      BTCPAY_CHAINS: "btc"
      BTCPAY_ROOTPATH: "/"
      BTCPAY_BTCEXPLORERURL: "http://nbxplorer:32838"
    volumes:
      - ./data/btcpay:/datadir
      - ./data/plugins:/root/.btcpayserver/Plugins
    network_mode: service:tailscale

  btcpay-db:
    image: postgres
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: "btcpay"
      POSTGRES_USER: "btcpay"
      POSTGRES_DB: "btcpay"
    volumes:
      - ./data/db:/var/lib/postgresql/data

  tailscale:
    image: tailscale/tailscale:latest
    hostname: btcpay
    environment:
      TS_AUTHKEY:
      TS_STATE_DIR: /var/lib/tailscale
      TS_SERVE_CONFIG: /config/serve.json
    volumes:
      - ./data/tailscale:/var/lib/tailscale
      - /dev/net/tun:/dev/net/tun
      - ./config:/config
    cap_add:
      - net_admin
      - sys_module
    restart: unless-stopped
    ports:
      - 100.107.87.40:23000:23000

networks:
  default:
    name: npm

I actually expose BTCPay in two different ways.

One is via Tailscale funnel with the following config in ./config/serve.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "TCP": {
    "443": {
      "HTTPS": true
    }
  },
  "Web": {
    "btcpay.my-tailnet.ts.net:443": {
      "Handlers": {
        "/": {
          "Proxy": "http://localhost:23000"
        }
      }
    }
  },
  "AllowFunnel": {
    "btcpay.my-tailnet.ts.net:443": true
  }
}

The other is via nginx-proxy-manager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
  app:
    image: jc21/nginx-proxy-manager:latest
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
      - 100.107.87.40:8080:81
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

networks:
  default:
    name: npm

You will need to setup a proxy host forwarding to your btcpay’s tailscale sidecar on port 23000 like http://btcpayserver-tailscale-1:23000. A DNS A-record for the Domain Name (btcpay.simonhaas.eu) is pointing to the vps’s public IP.

config

Inside BTCPay the config to accept on-chain Bitcoin payments is straight forward. Just connect a existing wallet or create a new one.

For Lightning payments I use strike.me because I do not want to manage my own lightning channels. For that you need to install the corresponding strike plugin within BTCPay. The the reset is again self explanatory. Create a API key for strike and add it to BTCPay.

Now you can create a Pay Button like mine and all the other features of BTCPay.

lightning addresses

Lighting addresses are like email addresses but for bitcoin - but simpler. In BTCPay under Lightning Address you can create a lightning address like simonhaas@btcpay.simonhaas.eu But just like my email address is not simonhaas@mail.simonhaas.eu but `simonhaas@simonhaas.eu I also would like to get rid of the subdomain of my btcpay server. It just looks cleaner and seperates the address from the underlying implementation. As with all things a basic understanding of how things works helps a lot.

BTCPay serves a specific JSON object under https://btcpay.simonhaas.eu/.well-known/lnurlp/simonhaas. Now we just have to serve the same content from https://simonhaas.eu/.well-known/lnurlp/simonhaas.

1
2
3
4
5
6
7
8
{
    "callback": "https://btcpay.simonhaas.eu/BTC/UILNURL/pay/lnaddress/simonhaas",
    "metadata": "[[\"text/identifier\",\"simonhaas@btcpay.simonhaas.eu\"],[\"text/plain\",\"Paid to Simon Haas\"]]",
    "tag": "payRequest",
    "minSendable": 1000,
    "maxSendable": 612000000000,
    "commentAllowed": 2000
}

I just have added a file to the static site acting as my homepage https://github.com/SimonHaas/simonhaas.github.io/blob/main/.well-known/lnurlp/simonhaas.

Now if someone wants to send you Bitcoin via Lightning you can just give them your lightning address and their wallet will know what to do: simonhaas@simonhaas.eu

If you only want a lightning address with your own domain but do not care about BTCPay you can achieve it by simply serving the JSON content of your strike lightning address. Strike provides me with the lightning address simonhaas@strike.me, https://strike.me/.well-known/lnurlp/simonhaas.

This post is licensed under CC BY 4.0 by the author.