So You Want to Accept Crypto Without Trusting Anyone
Here’s the problem with most “accept Bitcoin” solutions: they’re just Stripe with a Bitcoin logo slapped on. You hand your money to Bitpay, Coinbase Commerce, or Strike, and they handle custody, settlement, and KYC on your behalf. You’re not running anything. You’re not in control of anything. You’ve just replaced one intermediary with a slightly more ideologically confusing one.
BTCPay Server is the other path. Open-source, self-hosted, non-custodial Bitcoin (and Lightning) payment processing. Your server, your keys, your database, your node. Nobody in the middle taking a percentage or freezing your account because your product category triggered a compliance review.
It’s not simple to set up — I’m not going to lie to you. You’re running a full Bitcoin node, a Lightning implementation, a .NET application, and a Postgres database, ideally behind a reverse proxy. That’s a stack. But once it’s running, it’s genuinely yours.
This walkthrough covers what BTCPay actually is, what it costs you in hardware, and how to get it running without losing your mind.
What BTCPay Server Actually Is
BTCPay Server is a C# .NET application that acts as a payment coordinator between your store (website, POS, whatever) and the Bitcoin network. It talks to:
- Bitcoin Core — full node, validates the chain, watches for incoming payments on-chain
- LND or Core Lightning — Lightning Network daemon for instant, low-fee payments
- Postgres — stores invoices, stores, users, and all the bookkeeping
- nginx — reverse proxy, handles TLS
When a customer checks out, BTCPay generates a unique address (derived from your xpub — your wallet’s public key, not private key). It watches that address for payment, confirms it, and marks the invoice paid. You never expose your private keys to the server.
Supported currencies beyond BTC include Lightning Network (via LND or CLN), Liquid Network, Monero, and various ERC-20 tokens via plugins. The core install is Bitcoin + Lightning — that’s what this guide covers.
Hardware Reality Check
Before you get excited: a full Bitcoin node is not running on your Raspberry Pi 4 with a 32 GB SD card.
| Component | Minimum | Recommended |
|---|---|---|
| Disk | ~750 GB (full node) | 1 TB+ SSD |
| RAM | 4 GB | 8 GB |
| CPU | Any modern x64 | 4 cores helps |
| OS | Ubuntu 22.04/24.04 | Ubuntu 22.04 LTS |
The 750 GB figure is the current full chain size as of mid-2026. It grows. Plan for 1 TB minimum if you want headroom.
Pruned mode drops that to around 10 GB by discarding old block data. You can still receive payments — Bitcoin Core validates new transactions without the full history. The trade-off: you can’t serve historical chain data to other nodes, and some indexing features are unavailable. For a payment processor that just needs to watch addresses and confirm transactions, pruned mode is workable.
Initial Block Download (IBD) — syncing the chain from scratch — takes several days on typical home internet. Fast NVMe + good bandwidth: maybe 24-48 hours. Spinning disk on a slow connection: a week. Plan accordingly.
Install Path 1: BTCPay Docker (The Right Way)
The official and recommended install method uses the btcpayserver-docker repository, which generates a docker-compose.yml from environment variables. It handles all the service wiring for you.
Prerequisites
- A domain pointing at your server (
btcpay.yourdomain.com) - Open ports: 80, 443, 9735 (Lightning P2P)
- Docker and Docker Compose installed
- Root or sudo access
Setup
# Clone the BTCPay docker generatorgit clone https://github.com/btcpayserver/btcpayserver-docker /opt/btcpaycd /opt/btcpay
# Set environment variables (these drive the config generator)export BTCPAY_HOST="btcpay.yourdomain.com"export NBITCOIN_NETWORK="mainnet"export BTCPAYGEN_CRYPTO1="btc"export BTCPAYGEN_LIGHTNING="lnd"export BTCPAYGEN_REVERSEPROXY="nginx"export BTCPAYGEN_ADDITIONAL_FRAGMENTS="opt-save-storage"export BTCPAY_ENABLE_SSH="true"
# Run setup — this generates the compose file and starts everything. ./btcpay-setup.sh -iopt-save-storage enables pruned mode (keeps ~10 GB instead of 750+). Remove it if you want the full node.
BTCPAYGEN_LIGHTNING="lnd" deploys LND. Use clightning if you prefer Core Lightning.
The setup script writes a generated docker-compose.yml to /opt/btcpay/Generated/, starts the containers, and registers a systemd service (btcpayserver) that survives reboots.
Watching the sync
# Check container statusdocker ps
# Tail Bitcoin Core sync progressdocker logs -f generated_bitcoind_1 2>&1 | grep -E "progress|height"
# LND logsdocker logs -f generated_lnd_1Bitcoin Core logs UpdateTip: new best with a progress percentage. When it hits 1.000000, you’re synced.
Updates
cd /opt/btcpay. ./btcpay-update.shThat’s it. The update script pulls new images, regenerates the compose file if needed, and restarts cleanly.
Install Path 2: Manual (BYO Node)
If you’re already running Bitcoin Core and don’t want another instance eating your disk, you can connect BTCPay to your existing node. This is more work and easier to misconfigure, but legitimate if you know what you’re doing.
You’ll need to configure bitcoin.conf with RPC credentials and ensure txindex=1 is set (required for BTCPay’s address tracking). Then run BTCPay pointing at your node’s RPC endpoint instead of the bundled one.
Honestly, unless you have a specific reason, use the Docker path. The compose generator handles service discovery, TLS, and restart policies. DIY-ing all that takes an afternoon and introduces more failure surfaces.
Lightning Network Setup
Once the chain is synced, the LND container is running but has no channels. You need:
- A funded on-chain wallet (seed the LND wallet with sats)
- At least one channel opened to a well-connected peer
- Inbound liquidity if you want to receive payments (counterintuitive, but you need the other side to have balance)
Get your LND node info
# Execute inside the LND containerdocker exec -it generated_lnd_1 lncli getinfoNote your identity_pubkey and uris (your Lightning address). The URI includes your Tor address if Tor is configured — useful for inbound connectivity without exposing your home IP.
Autopilot: leave it off
LND’s autopilot automatically opens channels. Don’t enable it. Autopilot opens channels to random peers based on graph heuristics. You want to open channels intentionally — to exchanges, routing nodes, or services you actually use. Poorly chosen channels drain your on-chain fees and provide terrible routing.
Channel backups
LND has a channel.backup file that’s critical for recovery. Losing this means losing funds in open channels. Back it up somewhere not on the same machine:
# Copy from container to hostdocker cp generated_lnd_1:/root/.lnd/data/chain/bitcoin/mainnet/channel.backup \ /root/lnd_channel_backup_$(date +%Y%m%d).backup
# Then rsync/S3/wherever offsiteBTCPay also has built-in SCB (Static Channel Backup) management under Server Settings > Services > LND Seed Backup.
Setting Up Your First Store
Once BTCPay is running and synced:
- Navigate to
https://btcpay.yourdomain.com - Create an account (first account gets admin)
- Create a Store — give it a name, set your currency (for display/accounting, not settlement)
Link your wallet
BTCPay uses your xpub (extended public key) to derive payment addresses. This means the server can generate fresh addresses for every invoice without ever knowing your private key. Your hot wallet stays on your hardware wallet, phone, or wherever.
In your wallet software (Electrum, Sparrow, BlueWallet), find your xpub or zpub (for native SegWit) and paste it into BTCPay under Store > Wallet > Bitcoin.
BTCPay will generate addresses from that key. It never has signing capability — it can only watch.
Generate a test invoice
In the BTCPay dashboard, go to your store and create an invoice manually:
- Amount: whatever
- Currency: USD (or BTC)
- Click Create Invoice
You get a QR code and address. Send a small amount to verify the flow end to end before you go live.
Enable Lightning
In Store Settings > Lightning, connect to your LND node. If you’re on the Docker setup, BTCPay already knows the LND endpoint internally — just hit “Use internal node.”
Integrations and Checkout
Embed a checkout button
BTCPay has a JavaScript widget for payment buttons:
<script src="https://btcpay.yourdomain.com/modal/btcpay.js"></script><button data-url="https://btcpay.yourdomain.com" data-store-id="YOUR_STORE_ID" data-currency="USD" data-amount="25.00" data-description="Sticker Pack" onclick="window.btcpay.showInvoice(this)"> Pay with Bitcoin</button>For WooCommerce, Shopify, or other platforms there are plugins that handle the BTCPay integration. WooCommerce has a first-party BTCPay plugin. The Shopify integration works via the BTCPay Shopify app (connects to your self-hosted instance).
For custom integrations, use the Greenfield API (more on that below).
Point of Sale (POS) app
BTCPay includes a built-in POS interface at https://btcpay.yourdomain.com/your-store/pos. It shows your product catalog, accepts payment, and handles Lightning invoices. You can run it on a tablet at a market stall or pop-up. No third-party POS software needed.
Greenfield API Mode
BTCPay has two operational modes:
Classic mode — BTCPay processes the payment, customer sees the BTCPay checkout page. Your server handles everything end-to-end.
Greenfield API mode — you use the REST API to create invoices and handle webhooks programmatically, embedding BTCPay’s logic into your own UI.
The Greenfield API is documented at https://btcpay.yourdomain.com/docs:
# Create an invoice via APIcurl -X POST https://btcpay.yourdomain.com/api/v1/stores/STORE_ID/invoices \ -H "Authorization: token YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": "25.00", "currency": "USD", "metadata": {"orderId": "12345"}, "checkout": {"speedPolicy": "LowSpeed"} }'The response includes a checkoutLink (send the customer there) and an id for webhook matching. Set up a webhook in Store Settings > Webhooks pointing to your order fulfillment endpoint — BTCPay will POST invoice status updates as payments come in.
speedPolicy controls confirmation requirements:
LowSpeed— 6 confirmations (~1 hour). Most secure for high-value.MediumSpeed— 2 confirmations.HighSpeed— 1 confirmation.LightningOnly— instant, Lightning only.
Privacy Trade-Offs (Be Honest With Yourself)
Here’s the part people skip over: Bitcoin is not private by default.
Every on-chain transaction is publicly visible. If you receive a payment to an address linked to your identity (your website, your store, your name), that transaction is on the blockchain forever. Chain analysis firms and governments can trace funds. On-chain Bitcoin payments to a public business are, practically speaking, public records.
Lightning Network is substantially better. Payments are routed through encrypted channels, balances aren’t public, and individual transactions aren’t recorded on-chain. For a self-hosted payment setup where privacy matters, pushing customers toward Lightning is the right call.
Custodial Lightning defeats the purpose. If you accept Lightning via Strike, Wallet of Satoshi, or another custodial service, you’ve re-introduced the intermediary. They know your payment volume, they can freeze funds, and in many jurisdictions they’re running KYC. Self-hosting BTCPay with your own LND node keeps the whole stack under your control.
The Tax Reality
BTCPay doesn’t file taxes. It doesn’t report payments to anyone. That’s a feature, not a bug — and also your problem, not BTCPay’s.
In the US, the IRS treats crypto received as payment as ordinary income at fair market value on the date of receipt. When you later spend or sell that crypto, you may also owe capital gains tax on any appreciation. Most other jurisdictions have similar frameworks.
BTCPay’s reporting tools (under Store > Reports) give you invoice history with amounts and timestamps. Export that to your accountant. If you’re running a real business through this, get a real accountant who understands crypto. The “it’s decentralized so nobody knows” approach has gone badly for a lot of people.
Should You Bother?
Honestly? It depends on what you’re trying to solve.
Good fit:
- You run an online business and want to accept Bitcoin/Lightning without giving Coinbase a cut or signing up for KYC on a payment processor
- You’re selling digital goods and don’t want a Stripe account that can get frozen
- You’re ideologically committed to non-custodial, self-sovereign infrastructure and have the homelab to support it
- You’re running a pop-up or market stall and want a tablet-based POS that works offline (Lightning settles instantly)
Not a great fit:
- You just want to experiment — spin up a testnet instance instead, the mainnet setup is heavy
- You’re on a VPS with 20 GB of disk — full node won’t fit, pruned mode limits you, and VPS uptime SLAs are often too loose for payment infrastructure
- You expect plug-and-play — there’s a real operational burden here. Nodes go offline. Channels need rebalancing. The IBD takes days.
If you’ve already got a homelab machine with a 1 TB SSD sitting around, BTCPay is one of the more interesting things you can put on it. The stack is mature, the Docker setup is well-maintained, and running your own payment infrastructure is genuinely useful to understand even if you never charge a real customer through it.
The 2 AM version of yourself who gets hit with “payment processor account suspended pending review” will appreciate having thought this through beforehand.
Quick Reference
# Start/stop BTCPaysystemctl start btcpayserversystemctl stop btcpayserver
# Check all container statusdocker ps --filter "name=generated"
# Bitcoin Core sync statusdocker exec generated_bitcoind_1 bitcoin-cli getblockchaininfo | grep -E "blocks|headers|verificationprogress"
# LND wallet balancedocker exec generated_lnd_1 lncli walletbalance
# LND channel balancedocker exec generated_lnd_1 lncli channelbalance
# BTCPay logsjournalctl -u btcpayserver -f
# Rebuild/updatecd /opt/btcpay && . ./btcpay-update.shThe code is at github.com/btcpayserver/btcpayserver and the community on their Mattermost is genuinely helpful if you get stuck mid-IBD wondering why your Lightning node won’t sync. Fair warning: the rabbit hole is deep. Once you’re running your own payment node, the next logical step is running your own block explorer, then your own email server, and suddenly it’s 3 AM and you’re writing Ansible playbooks to back up your channel state to three different geographic locations. You’ve been warned.