Configuration reference
Every variable this service reads, generated from the Settings model that
reads them. The environment is the only configuration source; a .env file
next to the process is read too, and a real environment variable wins over it.
.env.example
is the annotated copy to start from — same variables, more operator prose. This
page is the one that cannot go out of date.
A required variable has no default and the process will not start without
it. Values marked secret are never logged and never printed by any
endpoint. Amounts are always integers in the asset's smallest unit — satoshis
for BTC and BTC_LN, micro-USDT for USDT_TRC20.
Process
| Variable |
Type |
Default |
Notes |
ENVIRONMENT |
development | test | staging | production |
production |
Also selects the API key prefix: development and test mint cpk_test_ keys, everything else mints cpk_live_ ones. |
DEBUG |
bool |
false |
Verbose errors and internals in responses. Refused unless ENVIRONMENT=development. |
LOG_LEVEL |
str |
INFO |
Python logging level for the structlog pipeline. |
LOG_JSON |
bool |
true |
One JSON object per line for a log collector; false gives human-readable console output. |
Database
| Variable |
Type |
Default |
Notes |
DATABASE_URL |
str |
— |
required — The ledger. Never the same instance as BTCPay's own database. |
DB_POOL_SIZE |
int |
5 |
≥ 1 — Postgres connections are the scarce resource on a 4GB box shared with BTCPay and bitcoind, so both pool numbers stay small. |
DB_MAX_OVERFLOW |
int |
5 |
≥ 0 — Connections opened beyond the pool when it is exhausted. |
DB_ECHO |
bool |
false |
Echo every SQL statement. Debugging only, and extremely noisy. |
HTTP server
| Variable |
Type |
Default |
Notes |
API_HOST |
str |
0.0.0.0 |
Bind address inside the container. The API publishes no ports in production; BTCPay and the platform reach it over the Docker network. |
API_PORT |
int |
8000 |
Port the API listens on inside the container. |
Idempotency-Key handling
| Variable |
Type |
Default |
Notes |
IDEMPOTENCY_STALE_SECONDS |
int |
60 |
≥ 1 — An in_progress idempotency row older than this may be reclaimed by a retry, so a crash between "row inserted" and "work done" cannot turn into a permanent 409 on a money endpoint. |
IDEMPOTENCY_TTL_HOURS |
int |
72 |
≥ 1 — How long completed idempotency records are kept before the purge job removes them. |
Asset seed values — BTC (satoshis)
| Variable |
Type |
Default |
Notes |
SEED_BTC_PAYMENT_METHOD |
str |
BTC-CHAIN |
BTCPay's payment method id for on-chain BTC. Version-dependent; the real value is discovered from the store at startup. |
SEED_BTC_WITHDRAWAL_AUTO_LIMIT |
int |
500000 |
≥ 0 — Withdrawals at or below this gross amount approve themselves. Above it they queue for an admin. |
SEED_BTC_WITHDRAWAL_DAILY_CAP |
int |
5000000 |
≥ 0 — Rolling 24-hour per-asset cap. Once the window sum reaches it every withdrawal goes to manual approval regardless of size — the control that actually bounds a stolen-API-key attack. |
SEED_BTC_WITHDRAWAL_USER_DAILY_CAP |
int |
(unset) |
Optional rolling 24-hour cap per user. Unset disables it. |
SEED_BTC_WITHDRAWAL_MIN |
int |
1 |
≥ 1 — Smallest accepted gross withdrawal. The dust rule applies on top. |
SEED_BTC_WITHDRAWAL_FLAT_FEE |
int |
0 |
≥ 0 — Flat service fee charged on top of the estimated miner fee. |
Asset seed values — Lightning, BTC_LN (satoshis)
| Variable |
Type |
Default |
Notes |
LIGHTNING_ENABLED |
bool |
false |
Lightning is off unless a deployment asks for it, and the default is the security decision rather than a convenience. Enabling it means the bootstrap requests btcpay.server.canuseinternallightningnode, which is a server-level BTCPay permission — the one kind this project otherwise refuses to hold. See the security model, threat 5. |
SEED_LN_PAYMENT_METHOD |
str |
BTC-LN |
BTCPay's Lightning payment method id. |
SEED_LN_WITHDRAWAL_AUTO_LIMIT |
int |
200000 |
≥ 0 — The same meaning as the BTC caps, over a different pot of money: channel balance, which the BTC caps say nothing about. |
SEED_LN_WITHDRAWAL_DAILY_CAP |
int |
2000000 |
≥ 0 — Rolling 24-hour cap on the Lightning float, in satoshis. |
SEED_LN_WITHDRAWAL_USER_DAILY_CAP |
int |
(unset) |
Optional rolling 24-hour cap per user. Unset disables it. |
SEED_LN_WITHDRAWAL_MIN |
int |
1000 |
≥ 1 — Above the flat fee on purpose: a minimum below it would accept requests that deliver nothing and be refused as dust a moment later. |
SEED_LN_WITHDRAWAL_FLAT_FEE |
int |
100 |
≥ 0 — A flat service charge, because a routing fee cannot be estimated before the route is found. What the route actually cost is journalled at settle time from the node, so this number is revenue and not a cost estimate. |
DEPOSIT_INVOICE_EXPIRY_MIN_LN |
int |
30 |
≥ 1 — Shorter than the on-chain window on purpose: an open BOLT11 invoice ties up inbound channel liquidity for as long as it lives. |
LN_PAYOUT_TIMEOUT_SECONDS |
int |
900 |
≥ 60 — How long a Lightning payout may sit in BTCPay's AwaitingPayment before this service cancels it. BTCPay's Lightning processor retries a payout it cannot route for as long as the invoice lives and never moves it to a failed state, so without a timeout the withdrawal stays submitted forever with the user's balance held. |
Asset seed values — USDT-TRC20 (micro-USDT)
| Variable |
Type |
Default |
Notes |
SEED_USDT_PAYMENT_METHOD |
str |
USDT_TRC20 |
BTCPay's payment method id from the USDt plugin. Version-dependent; discovered from the store at startup like the BTC one. |
SEED_USDT_WITHDRAWAL_AUTO_LIMIT |
int |
200000000 |
≥ 0 — USDT withdrawals are manual-only today, so this limit only starts to matter once an automated TRON signer exists. |
SEED_USDT_WITHDRAWAL_DAILY_CAP |
int |
2000000000 |
≥ 0 — Rolling 24-hour per-asset cap, in micro-USDT. |
SEED_USDT_WITHDRAWAL_USER_DAILY_CAP |
int |
(unset) |
Optional rolling 24-hour cap per user. Unset disables it. |
SEED_USDT_WITHDRAWAL_MIN |
int |
1 |
≥ 1 — Smallest accepted gross withdrawal, in micro-USDT. |
SEED_USDT_WITHDRAWAL_FLAT_FEE |
int |
1000000 |
≥ 0 — Flat fee covering the operator's TRX gas for a TRC-20 transfer. |
Withdrawal policy
| Variable |
Type |
Default |
Notes |
WITHDRAWAL_FEE_MODE |
deduct | absorb |
deduct |
deduct — the user receives gross minus the fee. absorb — the user receives gross and the operator pays the fee. |
USDT_AUTO_WITHDRAW |
bool |
false |
Refused at startup while it is true: the BTCPay USDt plugin has no payout handler, so nothing could send an auto-approved USDT withdrawal. |
Bitcoin network, fee estimation and deposit windows
| Variable |
Type |
Default |
Notes |
BITCOIN_NETWORK |
mainnet | testnet | signet | regtest |
mainnet |
Which network destination addresses are validated against. A production environment refuses to start on anything but mainnet. |
BTC_FEE_TARGET_BLOCKS |
int |
3 |
≥ 1 — Confirmation target for the fee estimate, in blocks. |
BTC_PAYOUT_VSIZE_VB |
int |
300 |
≥ 1 — Assumed payout size in virtual bytes. Raise it if the hot wallet holds many small deposit UTXOs; each extra P2WPKH input is roughly 68 vB. |
BTC_FALLBACK_FEE_SAT_PER_VB |
int |
20 |
≥ 1 — Used only when BTCPay and mempool.space both fail to answer. Reaching it logs an error, because every withdrawal is then priced off a guess. |
BTC_DUST_THRESHOLD_SAT |
int |
546 |
≥ 0 — A withdrawal whose net amount lands at or below this is refused before any hold is placed; the output would be unspendable. |
MEMPOOL_SPACE_URL |
str |
https://mempool.space/api/v1/fees/recommended |
Second fee source, tried when BTCPay's wallet estimate is unavailable. Unset disables it, which is right for a deployment that must make no outbound calls to third parties. |
WITHDRAWAL_SUBMIT_INTERVAL_SECONDS |
float |
10.0 |
> 0 — How often approved withdrawals are turned into BTCPay payouts. |
RECONCILE_WITHDRAWAL_INTERVAL_SECONDS |
float |
60.0 |
> 0 — Job B: how often submitted and broadcast payouts are re-checked. |
STUCK_SUBMITTING_SECONDS |
int |
300 |
≥ 30 — How long a withdrawal may sit in submitting with no payout reference before reconciliation works out what happened to it. |
DEPOSIT_INVOICE_EXPIRY_MIN_BTC |
int |
60 |
≥ 1 — Invoice checkout window. USDT is deliberately not shortened to recycle the address pool: a short window makes a late payment land on another user's reserved address. Size the pool up instead. |
DEPOSIT_INVOICE_EXPIRY_MIN_USDT |
int |
60 |
≥ 1 — The USDT checkout window. See the note on the BTC one above. |
DEPOSIT_MONITORING_MINUTES |
int |
1440 |
≥ 1 — How long after expiry BTCPay keeps attributing payments to an invoice, and therefore how long the deposit sweep keeps asking. Must be at least the invoice expiry. |
Worker schedule (the reconciliation loop)
| Variable |
Type |
Default |
Notes |
WEBHOOK_POLL_INTERVAL_SECONDS |
float |
5.0 |
> 0 — How often the worker drains the webhook_events queue. |
WEBHOOK_MAX_ATTEMPTS |
int |
10 |
≥ 1 — Attempts before an inbound webhook event is parked for an operator. |
RECONCILE_DEPOSIT_INTERVAL_SECONDS |
float |
120.0 |
> 0 — Job A: re-ask BTCPay about every deposit still worth asking about. |
RECONCILE_SETTLED_WINDOW_DAYS |
int |
7 |
≥ 1 — Fallback window for polling settled and expired deposits when BTCPay reported no monitoring expiry. |
RECONCILE_ORPHAN_SCAN_INTERVAL_SECONDS |
float |
86400.0 |
> 0 — Nightly page-through of the store's invoices looking for ours that have no local row — the case after restoring an older database backup. |
WALLET_SCAN_INTERVAL_SECONDS |
float |
600.0 |
> 0 — Wallet scan: flags coins that reached the hot wallet matching no deposit payment. The only detector for a payment to an address BTCPay stopped watching. |
BTCPay Server
| Variable |
Type |
Default |
Notes |
BTCPAY_URL |
str |
(unset) |
Greenfield base URL, container-to-container inside the Docker network. |
BTCPAY_API_KEY |
str |
(unset) |
secret — Restricted Greenfield API key scoped to the single store. Never server-admin. |
BTCPAY_STORE_ID |
str |
(unset) |
The store this service owns. |
BTCPAY_WEBHOOK_SECRET |
str |
(unset) |
secret — Webhook signing secret. Must differ from BTCPAY_API_KEY — the ingress path verifies an HMAC over raw bytes with this value. |
TRON / USDT-TRC20
| Variable |
Type |
Default |
Notes |
TRON_NETWORK |
mainnet | nile |
mainnet |
Selects the default TronGrid endpoint and the default USDT contract. |
TRONGRID_BASE_URL |
str |
(unset) |
Override the endpoint for your own node or a paid provider. Unset uses the network's TronGrid default. |
TRONGRID_API_KEY |
str |
(unset) |
secret — Required in production once a hot wallet address is set: keyless TronGrid access is throttled unpredictably, and what gets throttled is the check that a withdrawal really happened. |
USDT_CONTRACT_ADDRESS |
str |
(unset) |
Must match what the USDt plugin watches. The withdrawal verifier refuses a transfer of any other token. Unset uses the network default; both defaults were read off their chains on 2026-08-11 and answered USDT / 6, so setting this is about agreeing with your plugin rather than about distrusting the default. |
TRON_HOT_WALLET_ADDRESS |
str |
(unset) |
The address USDT is sent from. Verification compares every transfer's sender against it, so a wrong value rejects every genuine withdrawal. Unset disables the TRON jobs entirely, which is right for a BTC-only deployment. |
TRON_CONFIRMATIONS |
int |
19 |
≥ 1 — Blocks before a manual USDT withdrawal is treated as final. 19 is roughly TRON's solidified-block distance. |
GAS_MONITOR_INTERVAL_SECONDS |
float |
900.0 |
> 0 — How often the TRX gas balance of the hot wallet is checked. |
TRX_ALERT_THRESHOLD |
int |
200 |
≥ 0 — TRX balance below which an alert is raised. A wallet full of USDT and empty of TRX cannot send anything, and the symptom reads as "withdrawals are broken". |
USDT_AMOUNT_TOLERANCE_PCT |
float |
1.0 |
≥ 0 — How far a settled USDT deposit may deviate from the expected amount before it goes to review instead of crediting. Applies only when the platform supplied an expected amount; 0 disables it. |
Alerts
| Variable |
Type |
Default |
Notes |
NTFY_TOPIC_URL |
str |
(unset) |
Optional ntfy.sh (or compatible) topic URL. Alerts are always logged; this adds a push. |
TELEGRAM_BOT_TOKEN |
str |
(unset) |
secret — Optional Telegram bot. Both the token and the chat id must be set or the transport stays off. |
TELEGRAM_CHAT_ID |
str |
(unset) |
The chat alerts are sent to. Useless without the bot token. |
| Variable |
Type |
Default |
Notes |
PLATFORM_WEBHOOK_URL |
str |
(unset) |
Where deposit and withdrawal events are POSTed. Unset disables delivery, and events queue rather than being lost, so turning it on later ships the backlog. |
PLATFORM_WEBHOOK_SECRET |
str |
(unset) |
secret — HMAC-SHA256 key for the X-CPA-Signature header. The service refuses to start with a URL set and this unset: an unsigned webhook is one anybody can forge. |
OUTBOUND_DELIVERY_INTERVAL_SECONDS |
float |
15.0 |
> 0 — How often the delivery worker drains the queue. The per-event retry schedule is fixed in code and is not configurable. |
OUTBOUND_HTTP_TIMEOUT_SECONDS |
float |
10.0 |
> 0 — Per-request timeout for one delivery attempt. |
Reconciliation and health
| Variable |
Type |
Default |
Notes |
RECONCILE_INVARIANT_INTERVAL_SECONDS |
float |
3600.0 |
> 0 — Job C: how often the books are re-derived from the postings and custody is compared against the chain. This is the job that would notice an insolvency, so lengthening it lengthens how long one can go unnoticed. |
WORKER_HEARTBEAT_STALE_SECONDS |
int |
300 |
≥ 30 — A worker job whose last run is older than this reads as stale in /readyz. /healthz is process-and-database only, so this is how a dead worker is noticed. |
WEBHOOK_SIGNATURE_FAILURE_THRESHOLD |
int |
10 |
≥ 1 — Failed inbound signature verifications in the last hour before an alert. A spike means a misconfigured secret or somebody probing the endpoint. |
CUSTODY_TOLERANCE_UNITS |
int |
0 |
≥ 0 — How far custody may sit below user obligations before Job C calls it an insolvency signal, in the asset's smallest unit. 0 is correct and is the default: the expected shortfall is derived from in-flight postings, not from a tuned epsilon. |
Rules checked at startup
These are cross-field, so they are not visible in the table above. Each one is
a refusal to start, not a warning.
DEBUG=true is allowed only with ENVIRONMENT=development. Debug mode leaks internals into responses and logs.
DATABASE_URL must be a psycopg 3 PostgreSQL URL — postgresql:// or postgresql+psycopg://.
SEED_BTC_WITHDRAWAL_AUTO_LIMIT, SEED_LN_WITHDRAWAL_AUTO_LIMIT and SEED_USDT_WITHDRAWAL_AUTO_LIMIT may not exceed their matching *_DAILY_CAP. Otherwise one withdrawal could clear the 24-hour cap on its own.
BITCOIN_NETWORK must be mainnet when ENVIRONMENT=production. Anything else validates withdrawal addresses against the wrong chain.
TRONGRID_API_KEY is required in production once TRON_HOT_WALLET_ADDRESS is set. Keyless TronGrid access is throttled unpredictably, and what gets throttled is the check that a withdrawal really happened.
USDT_AUTO_WITHDRAW=true is refused outright. The BTCPay USDt plugin has no payout handler, so an auto-approved USDT withdrawal would stall with the user's balance held.
PLATFORM_WEBHOOK_SECRET is required once PLATFORM_WEBHOOK_URL is set. An unsigned webhook is one anybody can forge.
DEPOSIT_MONITORING_MINUTES may not be below DEPOSIT_INVOICE_EXPIRY_MIN_BTC. BTCPay would stop attributing payments before the invoice had even expired.