A Bitcoin-powered VPS platform. Customers pay with Bitcoin Lightning (or optionally fiat) to provision and renew virtual machines on Proxmox or LibVirt hypervisors.
- Payments
- Bitcoin Lightning via LND
- Fiat via RevolutPay
- LNURL-pay for VM renewal (scan-to-pay)
- Nostr Wallet Connect (NIP-47) for fully automatic renewal
- VM Backends
- Proxmox (production-ready)
- LibVirt (WIP)
- Network / DNS
- Mikrotik JSON-API — static ARP for anti-spoofing
- OVH Additional IP virtual MAC management
- Cloudflare API — forward A/AAAA and reverse PTR records
- Route-server management — BGP session, tunnel (GRE/VXLAN/WireGuard) and default-route visibility/control across RouterOS and Linux (BIRD/Pathvector over SSH) routers
- Nostr integration
- NIP-17 direct-message notifications
- NIP-05 identity server (
lnvps_nostr) - NIP-47 Nostr Wallet Connect for auto-renewal
- NIP-90 Data Vending Machine (DVM) for provisioning via Nostr
- AI support agent (
lnvps_agent)- Handles customer support requests with an OpenAI-compatible LLM and API-calling tools
- Email channel (IMAP IDLE inbox watching + SMTP replies) and Nostr kind-1 mention channel
- Security
- XDP eBPF SYN-flood rate limiter with self-upgrading
.debdaemon (lnvps_ebpf+lnvps_fw_service) - Per-VM firewall rules and default policy (applied on the host)
- Database field-level AES-GCM encryption for sensitive columns
- Cloudflare Turnstile captcha (optional)
- XDP eBPF SYN-flood rate limiter with self-upgrading
- Observability
- VM time-series metrics (CPU, memory, disk, network) from Proxmox RRD
- Network health monitoring with Prometheus metrics (
lnvps_health) - Grafana dashboard included (
grafana.json)
- Other
- WebSocket serial console proxy
- EU VAT validation via VIES
- Per-country tax rates
- Multi-region host support
- Kubernetes operator for Nostr domain Ingress management (
lnvps_operator) - OpenAPI/Swagger generation (
--features openapi) - Redis for shared state and horizontal scaling (optional)
| Crate | Binary | Description |
|---|---|---|
lnvps_api |
lnvps_api |
Main user-facing HTTP API + background worker |
lnvps_api_admin |
lnvps_api_admin |
Admin HTTP API with privileged operations |
lnvps_api_common |
— | Shared types, pricing engine, exchange rates, Redis queue, NIP-98 auth |
lnvps_db |
— | Database abstraction (MySQL/SQLx), migrations, field-level encryption |
lnvps_nostr |
lnvps_nostr |
Standalone NIP-05 identity server (/.well-known/nostr.json) |
lnvps_agent |
lnvps_agent |
AI support agent — answers support requests via email (IMAP/SMTP) and Nostr kind-1 mentions using an OpenAI-compatible LLM with API-calling tools |
lnvps_operator |
lnvps_operator |
Kubernetes operator — reconciles Nostr domain Ingress objects |
lnvps_health |
lnvps_health |
Network health monitor — TCP MSS/PMTU checks, DNS, Prometheus metrics |
lnvps_host_util |
— | Host-side helper utilities packaged for isolated Docker builds |
lnvps_e2e |
— | End-to-end integration test suite (spins up API + worker against a real DB/LND) |
lnvps_ebpf |
— | XDP eBPF SYN-flood rate limiter (kernel program) |
lnvps_fw_common |
— | Shared types between the eBPF datapath and the userspace loader |
lnvps_fw_service |
lnvps_fw_service |
Userspace loader + HTTP control API for the eBPF firewall, with self-upgrade from GitHub .deb releases |
The
lnvps_ebpf,lnvps_fw_common, andlnvps_fw_servicecrates live in the separatelnvps_fw/workspace (excluded from the root one) because the eBPF datapath needs its own nightlyrust-src+bpf-linkertoolchain and target.
cargo build --release -p lnvps_api
cargo build --release -p lnvps_api_admin
cargo build --release -p lnvps_nostr
cargo build --release -p lnvps_agent
cargo build --release -p lnvps_operator
cargo build --release -p lnvps_healthThe eBPF crates (lnvps_ebpf, lnvps_fw_common, lnvps_fw_service) live in the separate lnvps_fw/ workspace and require a nightly eBPF toolchain (rust-src + bpf-linker); build them separately. Tagging a vX.Y.Z release builds and attaches a .deb of lnvps_fw_service to the GitHub release, which running daemons install via their self-upgrade endpoint.
lnvps_api can run as a combined API + worker, or split into separate processes:
./lnvps_api --config config.yaml # API + worker (default)
./lnvps_api --config config.yaml --mode api # HTTP server only
./lnvps_api --config config.yaml --mode worker # background worker onlydocker compose up -d
# Starts: MariaDB on :3376, Redis on :6398, Krill (RPKI) on :3000# MySQL connection string (required)
db: "mysql://root:root@localhost:3376/lnvps"
# Public base URL used in webhook callbacks (required)
public-url: "https://api.example.com"
# HTTP listen address (default: 0.0.0.0:8000)
listen: "0.0.0.0:8000"
# Days after VM expiry before hard deletion
delete-after: 3
# Prevent VM creation/deletion
read-only: falselightning:
lnd:
url: "https://127.0.0.1:10003"
cert: "$HOME/.lnd/tls.cert"
macaroon: "$HOME/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"provisioner:
proxmox:
qemu:
machine: "q35"
os-type: "l26"
bridge: "vmbr0"
cpu: "kvm64"
kvm: false
arch: "x86_64"
# Per-NIC Proxmox firewall (optional)
firewall-config:
dhcp: true
enable: true
ip-filter: true
mac-filter: true
ndp: true
policy-in: "DROP" # ACCEPT | REJECT | DROP
policy-out: "ACCEPT"
# SSH access for host-side CLI commands (optional)
ssh:
key: "/root/.ssh/id_ed25519"
user: "root"
# MAC prefix for generated NICs (default: bc:24:11)
mac-prefix: "bc:24:11"
# LibVirt (WIP)
libvirt:
qemu:
machine: "q35"
os-type: "l26"
bridge: "vmbr0"
cpu: "kvm64"
kvm: falserevolut:
url: "https://merchant.revolut.com"
token: "my-revolut-api-token"
api-version: "2024-09-01"
public-key: "my-revolut-public-key"smtp:
admin: 1 # user ID to receive system alerts (optional)
server: "smtp.gmail.com"
from: "LNVPS <no-reply@example.com>" # optional
username: "no-reply@example.com"
password: "mypassword123"nostr:
relays:
- "wss://relay.snort.social"
- "wss://relay.damus.io"
- "wss://nos.lol"
nsec: "nsec1234xxx"dns:
# Zone ID for forward A/AAAA records (created as vm-<vmid>.<zone>)
forward-zone-id: "my-cloudflare-zone-id"
api:
cloudflare:
token: "my-api-token"redis:
url: "redis://localhost:6379"When configured, exchange rates, VM state cache, and the work queue all use Redis.
encryption:
key-file: "/etc/lnvps/encryption.key"
auto-generate: true # generate key if absentEncrypted fields: SSH key material, NWC connection strings, email addresses, host API tokens.
This implements EU VAT only. The seller's country is taken from the
company's own VAT number (tax_id) when set — that number is the company's VIES
registration and identifies the country it is registered in — otherwise from the
company's country_code. EU VAT applies only when that country is in the EU VAT
area; if it is outside the list (e.g. a US company), no tax is applied here —
other tax systems (such as US sales tax) are not handled.
When the seller is in the EU, standard rates for all member states are fetched
from an external source at startup and refreshed daily (cached in-memory by the
shared VatClient). The rate applied to a payment is then determined from the
seller's country and the customer:
- B2B with a stored (VIES-validated) VAT number: same country as seller → domestic VAT; another EU country → reverse charge (0%); outside the EU → out of scope (0%).
- B2C: place of supply is taken from the self-declared country, falling back to the IP-derived country. EU → that country's destination rate (OSS); non-EU → out of scope (0%).
- Undetermined (no country evidence): the seller's domestic rate is applied conservatively when the seller is in the EU, otherwise out of scope.
IP geolocation (for the fallback location signal) requires the optional
geoip-database setting pointing at a MaxMind GeoLite2/GeoIP2 Country .mmdb.
EU VAT numbers are validated against the VIES service before being stored.
Until the first successful rate refresh (or if the rate source is unreachable), no rates are known and VAT falls back to 0%.
Disclaimer: This VAT handling is an automated, best-effort determination from the available evidence and configuration — it is not tax or legal advice and makes no guarantee of compliance in any jurisdiction. Rates and validation come from third-party sources that may lag official changes. The operator is solely responsible for confirming the correct VAT/OSS treatment for their business and should have it reviewed by a qualified tax professional.
# Enables NIP-05 routing under this hostname
nostr-address-host: "nostr.example.com"captcha:
turnstile:
secret-key: "my-cloudflare-turnstile-secret"Standalone NIP-05 identity server. Reads domain/handle records from the shared database.
db: "mysql://root:root@localhost:3376/lnvps"
listen: "0.0.0.0:8001"Reconciles Kubernetes Ingress and cert-manager Certificate objects for Nostr domains.
db: "mysql://root:root@localhost:3376/lnvps"
namespace: "default"
reconcile-interval: 60 # seconds
error-retry-interval: 30
service-name: "lnvps-nostr"
port-name: "http"
cluster-issuer: "letsencrypt-prod"
ingress-class: "nginx"
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"Network health monitoring daemon. Runs TCP MSS/PMTU probes, DNS checks, exposes Prometheus metrics, and sends email alerts.
interval-secs: 600 # check interval
alert-cooldown-secs: 3600 # minimum time between repeated alerts
metrics:
enabled: true
bind: "127.0.0.1:9090" # Prometheus scrape endpoint (/metrics)
smtp:
host: "smtp.gmail.com"
port: 587
username: "alerts@example.com"
password: "password"
from: "alerts@example.com"
to: "admin@example.com"
mss-checks:
- name: "My Server"
host: "server1.example.com"
port: 443
expected-mss: 1460
expected-mss-v6: 1440 # optional, defaults to expected-mss - 20./lnvps_health --config config.yaml # run continuously
./lnvps_health --config config.yaml --once # run once and exitAI support agent. Watches an email inbox (IMAP IDLE) and/or Nostr kind-1 mentions, and answers
support requests using an OpenAI-compatible LLM with tools that call the LNVPS APIs. Config is loaded from a settings.yaml file in the working directory; all keys can also be overridden with
LNVPS_AGENT__* environment variables.
listen: "0.0.0.0:8080" # agent HTTP server (default)
admin-api-url: "https://api.example.com" # LNVPS admin API base URL (required)
user-api-url: "https://api.example.com" # LNVPS user API base URL (required)
nsec: "nsec1234xxx" # signs NIP-98 auth events / Nostr ops (required)
system-prompt: "You are LNVPS support..." # optional system prompt override
conversation-history-path: "/var/lib/lnvps-agent" # optional history store dir
openai: # OpenAI-compatible LLM (required)
base-url: "http://localhost:11434/v1" # e.g. Ollama, or https://api.openai.com/v1
api-key: "sk-..." # optional (not needed for Ollama)
model: "gpt-4o"
max-tokens: 2048
email: # email channel (optional)
imap-server: "imap.gmail.com:993"
imap-username: "support@example.com"
imap-password: "app-password"
imap-mailbox: "INBOX" # optional
smtp-server: "smtp.gmail.com:587"
smtp-username: "support@example.com"
smtp-password: "app-password"
smtp-from: "support@example.com"
smtp-from-name: "LNVPS Support" # optional
kind1: # Nostr kind-1 mention channel (optional)
relays:
- "wss://relay.damus.io"
mention-pubkeys: [] # hex pubkeys to watch; defaults to the bot's own
poll-interval-secs: 30