This guide covers setting up the Grin node, wallet, and BTCPay Server plugin for accepting Grin payments.
- BTCPay Server v2.3.5+ (Docker or bare metal)
- A Linux server to run grin-wallet (can be the same machine as BTCPay or separate)
BTCPay Server (plugin)
| \
| v3 encrypted \ plain JSON-RPC (optional,
| JSON-RPC \ for sync status monitoring)
| \
grin-wallet (port 3420) Grin Node (API port 3413)
| /
| JSON-RPC /
| /
Grin Node (P2P port 3414)
|
| P2P
|
Grin Network
The plugin communicates with grin-wallet via its Owner API for all wallet operations. Optionally, if the Node API URL is configured, the plugin also queries the Grin node's get_status endpoint directly for detailed sync progress (percentage, sync phase).
The Owner API uses v3 encrypted JSON-RPC. Every session starts with an ECDH key exchange:
- Plugin generates an ephemeral secp256k1 keypair
- Plugin sends its public key to
init_secure_api - Wallet returns its public key
- Both sides derive a shared secret (ECDH x-coordinate)
- All subsequent RPC calls are encrypted with AES-256-GCM using this shared key
- Plugin calls
open_wallet(encrypted) with the wallet password to get a session token - All further calls include the session token
This means even over plaintext HTTP, the RPC payload is encrypted end-to-end. The API secret (Basic Auth) protects against unauthorized access to the endpoint itself.
If the wallet restarts, the shared key becomes invalid. The plugin detects this (AES decryption failure) and automatically re-establishes the session.
Download grin and grin-wallet binaries from grin.mw or build from source.
# Install to /usr/local/bin (or anywhere on PATH)
sudo cp grin /usr/local/bin/
sudo cp grin-wallet /usr/local/bin/
# Create a dedicated user
sudo useradd -m -s /bin/bash grinYou have two options: run your own node or use a remote node.
# As the grin user
sudo -u grin bash
cd ~
# Initialize node config
grin server config
# Edit grin-server.toml:
# - Set api_http_addr = "127.0.0.1:3413"
# - Disable TUI: run_tui = false (for headless/systemd)
# Start the node (first sync takes several hours)
grin server runInstall as a systemd service (see contrib/systemd/):
sudo cp contrib/systemd/grin-node.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now grin-nodeMonitor sync progress:
curl -s http://127.0.0.1:3413/v2/owner \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"get_status","params":{}}' | python3 -m json.toolThe node is synced when sync_status is "no_sync" and tip.height matches the network height.
If you don't want to run your own node, you can point grin-wallet at a public node. Edit grin-wallet.toml after wallet init:
check_node_api_http_addr = "https://grincoin.org"Known public nodes:
https://grincoin.org(community node)https://grinnode.live:3413(community node)
Note: Using a remote node means trusting that node for transaction broadcasting and blockchain data. For maximum security, run your own node.
# As the grin user
sudo -u grin bash
cd ~/.grin/main # or wherever you want wallet data
# Initialize the wallet (you'll set a password)
grin-wallet init
# SAVE the recovery phrase securely
# SAVE the password — you'll need it for the BTCPay plugin config
# Note the API secret (generated automatically):
cat .owner_api_secretEdit ~/.grin/main/grin-wallet.toml:
[wallet]
# Owner API settings
owner_api_listen_port = 3420
owner_api_listen_interface = "127.0.0.1" # or "0.0.0.0" if BTCPay is on another machine
# API secret for Basic Auth
api_secret_path = "/home/grin/.grin/main/.owner_api_secret"
# Node connection
check_node_api_http_addr = "http://127.0.0.1:3413" # or remote node URLgrin-wallet owner_apiOr install as a systemd service:
sudo cp contrib/systemd/grin-wallet.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now grin-wallet# Quick test — should return a JSON-RPC response
curl -s http://127.0.0.1:3420/v3/owner \
-u "grin:$(cat /home/grin/.grin/main/.owner_api_secret)" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"init_secure_api","params":{"ecdh_pubkey":"02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}'If you get a JSON response with "result": {"Ok": "02..."}, the wallet API is working.
If BTCPay Server runs in Docker and grin-wallet runs on the host, the container can't reach 127.0.0.1:3420 on the host. You need to expose the wallet API to the Docker bridge network.
Forward the Docker gateway IP to localhost:
# Find the Docker bridge gateway IP (usually 172.17.0.1 or 172.18.0.1)
docker network inspect bridge -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
# Create a socat proxy
socat TCP-LISTEN:3420,fork,bind=172.18.0.1,reuseaddr TCP:127.0.0.1:3420Install as a systemd service:
# /etc/systemd/system/grin-wallet-proxy.service
[Unit]
Description=Forward grin-wallet owner API to Docker network
After=grin-wallet.service
BindsTo=grin-wallet.service
PartOf=grin-wallet.service
[Service]
Type=simple
ExecStart=/usr/bin/socat TCP-LISTEN:3420,fork,bind=172.18.0.1,reuseaddr TCP:127.0.0.1:3420
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetIf running your own node and want sync status monitoring in the plugin, also proxy the node API:
# /etc/systemd/system/grin-node-proxy.service
[Unit]
Description=Forward grin node API to Docker network
After=grin-node.service
BindsTo=grin-node.service
PartOf=grin-node.service
[Service]
Type=simple
ExecStart=/usr/bin/socat TCP-LISTEN:3413,fork,bind=172.18.0.1,reuseaddr TCP:127.0.0.1:3413
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetIn BTCPay plugin settings:
- Owner API URL:
http://172.18.0.1:3420 - Node API URL (optional):
http://172.18.0.1:3413(enables sync percentage display)
Note: Both the grin-wallet Owner API and the grin node API bind to 127.0.0.1 only. The owner_api_listen_interface setting in grin-wallet.toml exists in the config template but is not read by the code (owner_api_listen_addr() in config/src/types.rs hardcodes 127.0.0.1). The socat proxy is the correct way to expose them to Docker.
- Download the latest
.btcpayfile from GitHub Releases - Go to BTCPay Server Settings > Plugins > Upload Plugin
- Upload the
.btcpayfile - BTCPay will restart and load the plugin
If you have SSH access to the server, you can deploy directly to the plugin volume:
# Download and extract the release
cd /tmp
curl -sL https://github.com/Such-Software/btcpayserver-grin-plugin/releases/download/v1.0.4/1.0.4.0.tar.xz -o grin-plugin.tar.xz
tar xf grin-plugin.tar.xz
# Extract the .btcpay zip into the plugins volume
PLUGIN_DIR=/var/lib/docker/volumes/generated_btcpay_pluginsdir/_data/BTCPayServer.Plugins.Grin
mkdir -p "$PLUGIN_DIR"
python3 -c "import zipfile; zipfile.ZipFile('/tmp/1.0.4.0/BTCPayServer.Plugins.Grin.btcpay').extractall('$PLUGIN_DIR')"
# Restart BTCPay to load the plugin
docker restart generated_btcpayserver_1See README.md for build instructions using PluginPacker.
- In BTCPay, go to your store's settings
- Click Grin in the left sidebar
- Enter:
- Owner API URL:
http://127.0.0.1:3420(orhttp://172.18.0.1:3420for Docker) - Wallet Password: the password you set during
grin-wallet init - API Secret: contents of
.owner_api_secret - Node API URL (optional):
http://127.0.0.1:3413(or Docker gateway URL) — enables sync percentage in the status panel
- Owner API URL:
- Set Minimum Confirmations (default: 10, roughly 10 minutes)
- Check Enable Grin Payments and click Save
- Click Test Connection — should show the current node height
The ECDH handshake succeeded (URL and API secret are correct) but open_wallet failed. The wallet password in plugin settings doesn't match the password used during grin-wallet init.
Fix: Re-enter the correct password in plugin settings, or re-initialize the wallet with the correct password:
sudo -u grin bash
cd ~/.grin/main
rm -rf wallet_data
grin-wallet init # set the password you want
# Then update .owner_api_secret in BTCPay settings (it gets regenerated)
systemctl restart grin-walletThe plugin can't reach the wallet API at all.
- Check that grin-wallet owner_api is running:
systemctl status grin-wallet - Check the URL in plugin settings matches where the wallet is listening
- If using Docker, verify the socat proxy is running:
systemctl status grin-wallet-proxy - Test connectivity:
curl http://<url>:3420/v3/owner
The Grin node hasn't finished syncing. Initial sync can take several hours depending on your connection. Check sync status:
curl -s http://127.0.0.1:3413/v2/owner \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"get_status","params":{}}' | python3 -m json.toolThe wallet will work once sync_status shows "no_sync".
- Check that the Grin node is synced (see above)
- The background monitor polls every 30 seconds — confirmations update automatically
- Minimum confirmations default is 10 (~10 minutes)
- Check BTCPay logs:
docker logs generated_btcpayserver_1 --tail 100 | grep -i grin
- Wallet credentials (password, API secret) are stored in BTCPay's PostgreSQL database
- The Owner API uses encrypted RPC — even over HTTP, payloads are AES-256-GCM encrypted
- The plugin never holds private keys — all signing happens in grin-wallet
- Bind the Owner API to localhost (
127.0.0.1) and use socat for Docker access - Don't expose port 3420 to the internet — the API has full wallet control
- Self-hosted BTCPay = self-custodial. Third-party BTCPay = you're trusting the operator with wallet credentials (same trust model as Lightning in BTCPay)