agent-bitcoin 26.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ """
2
+ Agent-Bitcoin SDK
3
+ =================
4
+
5
+ A Python library for Lightning Network payments between autonomous AI agents.
6
+ """
7
+
8
+ __version__ = "26.2.0"
9
+
10
+ # Core
11
+ from .client import AgentBitcoinClient, create_client
12
+ from .constants import (
13
+ DEFAULT_L402_PRICE_SATS,
14
+ DEFAULT_MAX_PAYMENT_SATS,
15
+ DEFAULT_MIN_PAYMENT_SATS,
16
+ )
17
+ from .l402 import L402Client, L402Challenge, parse_www_authenticate
18
+ from .models import LightningConfig, Invoice, InvoiceQuote, PayerDecisionInputs
19
+
20
+ # Exceptions
21
+ from .exceptions import (
22
+ AgentBitcoinError,
23
+ InvoiceCreationError,
24
+ PaymentError,
25
+ MacaroonError,
26
+ InsufficientBalanceError,
27
+ NoRouteError,
28
+ )
29
+
30
+ # Intelligent Agents (kept for future use)
31
+ from .agents.payment_decision import (
32
+ PaymentDecisionAgent,
33
+ create_payment_decision_agent,
34
+ create_grok_payment_decision_agent,
35
+ PaymentDecision,
36
+ )
37
+
38
+ # Main public API
39
+ __all__ = [
40
+ # Core
41
+ "AgentBitcoinClient",
42
+ "LightningConfig",
43
+ "Invoice",
44
+ "InvoiceQuote",
45
+ "PayerDecisionInputs",
46
+ "create_client",
47
+ "DEFAULT_MIN_PAYMENT_SATS",
48
+ "DEFAULT_MAX_PAYMENT_SATS",
49
+ "DEFAULT_L402_PRICE_SATS",
50
+ "L402Client",
51
+ "L402Challenge",
52
+ "parse_www_authenticate",
53
+ # Exceptions
54
+ "AgentBitcoinError",
55
+ "InvoiceCreationError",
56
+ "PaymentError",
57
+ "MacaroonError",
58
+ "InsufficientBalanceError",
59
+ "NoRouteError",
60
+ # Intelligent Agents
61
+ "PaymentDecisionAgent",
62
+ "create_payment_decision_agent",
63
+ "create_grok_payment_decision_agent",
64
+ "PaymentDecision",
65
+ ]
@@ -0,0 +1,15 @@
1
+ # agent_bitcoin/agents/__init__.py
2
+
3
+ from .payment_decision import (
4
+ PaymentDecisionAgent,
5
+ BitcoinLNDAgent,
6
+ create_grok_payment_decision_agent,
7
+ create_grok_bitcoin_lnd_agent,
8
+ )
9
+
10
+ __all__ = [
11
+ "PaymentDecisionAgent",
12
+ "BitcoinLNDAgent",
13
+ "create_grok_payment_decision_agent",
14
+ "create_grok_bitcoin_lnd_agent",
15
+ ]
File without changes
@@ -0,0 +1,244 @@
1
+ """Payment decision agents with coded policy limits (not prompt-only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from enum import Enum
8
+ from typing import Any, Optional
9
+
10
+ from langchain_core.messages import HumanMessage, SystemMessage
11
+ from langchain_xai import ChatXAI
12
+
13
+ from agent_bitcoin.constants import (
14
+ min_payment_sats,
15
+ payment_decision_max_sats,
16
+ )
17
+ from agent_bitcoin.prompts import (
18
+ BITCOIN_LND_SYSTEM_PROMPT,
19
+ PAYMENT_DECISION_DEFAULT_INSTRUCTIONS,
20
+ PAYMENT_DECISION_SYSTEM_PROMPT,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class PaymentDecision(Enum):
27
+ PAY = "PAY"
28
+ REJECT = "REJECT"
29
+ CONFIRM_REQUIRED = "CONFIRM_REQUIRED"
30
+
31
+
32
+ class PaymentDecisionAgent:
33
+ """
34
+ Conservative gatekeeper for Lightning invoice payments.
35
+
36
+ Policy is enforced in code first; the LLM only runs if hard limits pass.
37
+ This agent never executes payments — callers must act on the returned decision.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ api_key: Optional[str] = None,
43
+ model: str = "grok-4-1-fast-reasoning",
44
+ min_sats: Optional[int] = None,
45
+ max_sats: Optional[int] = None,
46
+ confirm_above_sats: Optional[int] = None,
47
+ ):
48
+ self.llm = ChatXAI(
49
+ model=model,
50
+ api_key=api_key,
51
+ temperature=0.1,
52
+ )
53
+ self.system_prompt = PAYMENT_DECISION_SYSTEM_PROMPT
54
+ self.default_instructions = PAYMENT_DECISION_DEFAULT_INSTRUCTIONS
55
+
56
+ # Coded limits (shared defaults; constructor / env overrides)
57
+ self.min_sats = min_sats if min_sats is not None else min_payment_sats()
58
+ self.max_sats = (
59
+ max_sats if max_sats is not None else payment_decision_max_sats()
60
+ )
61
+ # Amounts above this require human confirmation (no automatic PAY)
62
+ if confirm_above_sats is not None:
63
+ self.confirm_above_sats = confirm_above_sats
64
+ else:
65
+ raw = os.getenv("PAYMENT_DECISION_CONFIRM_ABOVE_SATS", "").strip()
66
+ self.confirm_above_sats = int(raw) if raw else None
67
+
68
+ def _parse_amount(self, invoice_data: dict) -> Optional[int]:
69
+ raw = invoice_data.get("amount_sats")
70
+ if raw is None:
71
+ return None
72
+ try:
73
+ return int(raw)
74
+ except (TypeError, ValueError):
75
+ return None
76
+
77
+ def _policy_gate(self, amount: Optional[int]) -> Optional[dict[str, Any]]:
78
+ """Return a reject/confirm result if policy blocks LLM, else None."""
79
+ if amount is None:
80
+ return {
81
+ "decision": PaymentDecision.REJECT.value,
82
+ "reasoning": "Policy: missing or invalid amount_sats; refusing to evaluate.",
83
+ "blocked_by_policy": True,
84
+ "policy_code": "INVALID_AMOUNT",
85
+ "amount_sats": None,
86
+ }
87
+ if amount < self.min_sats:
88
+ return {
89
+ "decision": PaymentDecision.REJECT.value,
90
+ "reasoning": (
91
+ f"Policy: amount {amount} sats is below minimum {self.min_sats} sats."
92
+ ),
93
+ "blocked_by_policy": True,
94
+ "policy_code": "BELOW_MINIMUM",
95
+ "amount_sats": amount,
96
+ }
97
+ if amount > self.max_sats:
98
+ return {
99
+ "decision": PaymentDecision.REJECT.value,
100
+ "reasoning": (
101
+ f"Policy: amount {amount} sats exceeds hard maximum "
102
+ f"{self.max_sats} sats (PAYMENT_DECISION_MAX_SATS)."
103
+ ),
104
+ "blocked_by_policy": True,
105
+ "policy_code": "ABOVE_MAXIMUM",
106
+ "amount_sats": amount,
107
+ }
108
+ if self.confirm_above_sats is not None and amount > self.confirm_above_sats:
109
+ return {
110
+ "decision": PaymentDecision.CONFIRM_REQUIRED.value,
111
+ "reasoning": (
112
+ f"Policy: amount {amount} sats is above confirmation threshold "
113
+ f"{self.confirm_above_sats} sats. Human approval required before PAY."
114
+ ),
115
+ "blocked_by_policy": True,
116
+ "policy_code": "CONFIRM_REQUIRED",
117
+ "amount_sats": amount,
118
+ }
119
+ return None
120
+
121
+ def decide_payment(self, invoice_data: dict, context: str = "") -> dict:
122
+ amount = self._parse_amount(invoice_data)
123
+ gate = self._policy_gate(amount)
124
+ if gate is not None:
125
+ result = {
126
+ **gate,
127
+ "raw_response": None,
128
+ "policy": {
129
+ "min_sats": self.min_sats,
130
+ "max_sats": self.max_sats,
131
+ "confirm_above_sats": self.confirm_above_sats,
132
+ },
133
+ }
134
+ logger.info(
135
+ "payment_decision policy_block code=%s decision=%s amount=%s",
136
+ result.get("policy_code"),
137
+ result["decision"],
138
+ amount,
139
+ )
140
+ return result
141
+
142
+ # Never put full BOLT11 / secrets in the prompt
143
+ pay_req = str(invoice_data.get("payment_request") or "")
144
+ pay_req_preview = (pay_req[:48] + "…") if len(pay_req) > 48 else pay_req
145
+ memo = str(invoice_data.get("memo") or "No memo")[:200]
146
+ ctx = (context or "No additional context")[:500]
147
+
148
+ prompt = f"""Invoice details:
149
+ Amount: {amount} sats
150
+ Memo: {memo}
151
+ Payment Request (truncated): {pay_req_preview or "N/A"}
152
+
153
+ Context: {ctx}
154
+
155
+ Hard policy already applied (do not override):
156
+ - Minimum: {self.min_sats} sats
157
+ - Maximum: {self.max_sats} sats
158
+ {f"- Confirm above: {self.confirm_above_sats} sats" if self.confirm_above_sats else ""}
159
+
160
+ {self.default_instructions}
161
+
162
+ Should we pay this invoice? Respond with clear reasoning and final decision (PAY / REJECT).
163
+ Do not invent payment execution; only decide."""
164
+
165
+ messages = [
166
+ SystemMessage(content=self.system_prompt),
167
+ HumanMessage(content=prompt),
168
+ ]
169
+
170
+ response = self.llm.invoke(messages)
171
+ text = response.content if hasattr(response, "content") else str(response)
172
+ text_upper = text.upper()
173
+
174
+ # Prefer explicit REJECT if both words appear
175
+ if "REJECT" in text_upper and "PAY" in text_upper:
176
+ # last occurrence wins lightly: if REJECT after last PAY → reject
177
+ decision = (
178
+ PaymentDecision.REJECT.value
179
+ if text_upper.rfind("REJECT") > text_upper.rfind("PAY")
180
+ else PaymentDecision.PAY.value
181
+ )
182
+ elif "REJECT" in text_upper:
183
+ decision = PaymentDecision.REJECT.value
184
+ elif "PAY" in text_upper:
185
+ decision = PaymentDecision.PAY.value
186
+ else:
187
+ decision = PaymentDecision.REJECT.value # fail closed
188
+
189
+ result = {
190
+ "decision": decision,
191
+ "reasoning": text,
192
+ "raw_response": text,
193
+ "blocked_by_policy": False,
194
+ "policy_code": None,
195
+ "amount_sats": amount,
196
+ "policy": {
197
+ "min_sats": self.min_sats,
198
+ "max_sats": self.max_sats,
199
+ "confirm_above_sats": self.confirm_above_sats,
200
+ },
201
+ }
202
+ logger.info(
203
+ "payment_decision llm decision=%s amount=%s",
204
+ decision,
205
+ amount,
206
+ )
207
+ return result
208
+
209
+
210
+ class BitcoinLNDAgent:
211
+ """Agent for the counterparty node (agent-bitcoin-lnd). Prompt helper only."""
212
+
213
+ def __init__(
214
+ self, api_key: Optional[str] = None, model: str = "grok-4-1-fast-reasoning"
215
+ ):
216
+ self.llm = ChatXAI(
217
+ model=model,
218
+ api_key=api_key,
219
+ temperature=0.3,
220
+ )
221
+ self.system_prompt = BITCOIN_LND_SYSTEM_PROMPT
222
+
223
+ def create_invoice_prompt(self, amount_sats: int, memo: str) -> str:
224
+ return (
225
+ f"Create a Lightning invoice for {amount_sats} sats "
226
+ f"with memo: '{memo}'. Be professional and clear."
227
+ )
228
+
229
+
230
+ def create_grok_payment_decision_agent(
231
+ api_key: Optional[str] = None,
232
+ **policy_kwargs,
233
+ ):
234
+ """Create Grok-powered payment decision agent (optional policy kwargs)."""
235
+ return PaymentDecisionAgent(api_key=api_key, **policy_kwargs)
236
+
237
+
238
+ def create_grok_bitcoin_lnd_agent(api_key: Optional[str] = None):
239
+ """Create Grok-powered agent for the counterparty LND node."""
240
+ return BitcoinLNDAgent(api_key=api_key)
241
+
242
+
243
+ # Alias for backward compatibility with __init__.py
244
+ create_payment_decision_agent = create_grok_payment_decision_agent
@@ -0,0 +1,258 @@
1
+ from dotenv import load_dotenv
2
+
3
+ from .constants import (
4
+ autopay_allowed,
5
+ fee_send_allowed,
6
+ max_daily_payment_sats,
7
+ max_payment_sats,
8
+ min_payment_sats,
9
+ current_network,
10
+ )
11
+ from .lightning import LNDClient
12
+ from .models import (
13
+ Invoice,
14
+ InvoiceQuote,
15
+ PayerDecisionInputs,
16
+ PaymentResult,
17
+ OnChainSendResult,
18
+ LightningBalance,
19
+ ChannelBalance,
20
+ )
21
+ from .spend_ledger import assert_can_spend, record_spend
22
+
23
+ load_dotenv()
24
+
25
+
26
+ def _invoice_amount_sats(decoded: dict) -> int:
27
+ """Extract sat amount from lncli/gRPC decodepayreq dict."""
28
+ for key in ("num_satoshis", "num_sats", "amt_sat"):
29
+ if decoded.get(key) is not None:
30
+ try:
31
+ return int(decoded[key])
32
+ except (TypeError, ValueError):
33
+ pass
34
+ for key in ("num_msat", "amt_msat"):
35
+ if decoded.get(key) is not None:
36
+ try:
37
+ return int(decoded[key]) // 1000
38
+ except (TypeError, ValueError):
39
+ pass
40
+ return 0
41
+
42
+
43
+ class AgentBitcoinClient:
44
+ def __init__(self):
45
+ self.lnd = LNDClient()
46
+
47
+ self.min_payment_sats = min_payment_sats()
48
+ self.max_payment_sats = max_payment_sats()
49
+ self.max_daily_payment_sats = max_daily_payment_sats()
50
+
51
+ def create_invoice(
52
+ self, memo: str, amount_sats: int, expiry_seconds: int = 3600
53
+ ) -> Invoice:
54
+ if amount_sats < self.min_payment_sats:
55
+ raise ValueError(f"Minimum payment is {self.min_payment_sats} sats")
56
+ if amount_sats > self.max_payment_sats:
57
+ raise ValueError(f"Maximum payment is {self.max_payment_sats} sats")
58
+ return self.lnd.create_invoice(memo, int(amount_sats), expiry_seconds)
59
+
60
+ def create_invoice_quote(
61
+ self,
62
+ memo: str,
63
+ amount_sats: int,
64
+ expiry_seconds: int = 3600,
65
+ ) -> InvoiceQuote:
66
+ """
67
+ Payee: create one BOLT11 for the requested amount.
68
+
69
+ amount_sats is the service amount (min/max apply). BOLT11 and
70
+ total_cost_sats equal that amount. There is no platform fee.
71
+ """
72
+ if amount_sats < self.min_payment_sats:
73
+ raise ValueError(f"Minimum payment is {self.min_payment_sats} sats")
74
+ if amount_sats > self.max_payment_sats:
75
+ raise ValueError(f"Maximum payment is {self.max_payment_sats} sats")
76
+ total = int(amount_sats)
77
+ inv = self.lnd.create_invoice(memo, total, expiry_seconds)
78
+ return InvoiceQuote(
79
+ payment_request=inv.payment_request,
80
+ amount_sats=total,
81
+ total_cost_sats=total,
82
+ memo=memo or "",
83
+ r_hash=inv.r_hash,
84
+ payment_hash=inv.payment_hash,
85
+ network=current_network(),
86
+ )
87
+
88
+ def validate_invoice_quote(self, quote: InvoiceQuote | dict) -> InvoiceQuote:
89
+ """
90
+ Payer: check quote internal consistency and BOLT11 amount match.
91
+ Raises ValueError if invalid.
92
+ """
93
+ if isinstance(quote, dict):
94
+ quote = InvoiceQuote.model_validate(quote)
95
+ if quote.amount_sats < 0:
96
+ raise ValueError("amount_sats must be >= 0")
97
+ if quote.total_cost_sats != quote.amount_sats:
98
+ raise ValueError(
99
+ f"total_cost_sats={quote.total_cost_sats} != "
100
+ f"amount_sats={quote.amount_sats}"
101
+ )
102
+ if not quote.payment_request:
103
+ raise ValueError("payment_request is required")
104
+
105
+ decoded = self.lnd.decode_pay_req(quote.payment_request.strip())
106
+ bolt_amt = _invoice_amount_sats(decoded)
107
+ if bolt_amt != quote.total_cost_sats:
108
+ raise ValueError(
109
+ f"BOLT11 amount {bolt_amt} does not match quote total_cost_sats "
110
+ f"{quote.total_cost_sats} (requested {quote.amount_sats})"
111
+ )
112
+ return quote
113
+
114
+ def build_payer_decision_inputs(
115
+ self,
116
+ quote: InvoiceQuote | dict,
117
+ routing_fee_limit_sats: int = 200,
118
+ ) -> PayerDecisionInputs:
119
+ """
120
+ Payer: produce decision inputs for PaymentDecisionAgent / budget checks.
121
+ Does not pay. Sets quote_valid False on validation failure (no raise).
122
+ """
123
+ if isinstance(quote, dict):
124
+ try:
125
+ q = InvoiceQuote.model_validate(quote)
126
+ except Exception as e:
127
+ return PayerDecisionInputs(
128
+ payment_request=str(quote.get("payment_request") or ""),
129
+ amount_sats=int(quote.get("amount_sats") or 0),
130
+ total_cost_sats=int(quote.get("total_cost_sats") or 0),
131
+ routing_fee_limit_sats=int(routing_fee_limit_sats),
132
+ quote_valid=False,
133
+ validation_error=str(e),
134
+ )
135
+ else:
136
+ q = quote
137
+
138
+ try:
139
+ q = self.validate_invoice_quote(q)
140
+ decoded = self.lnd.decode_pay_req(q.payment_request.strip())
141
+ dest = str(decoded.get("destination") or "")
142
+ return PayerDecisionInputs(
143
+ payment_request=q.payment_request,
144
+ amount_sats=q.amount_sats,
145
+ total_cost_sats=q.total_cost_sats,
146
+ routing_fee_limit_sats=int(routing_fee_limit_sats),
147
+ quote_valid=True,
148
+ validation_error=None,
149
+ memo=q.memo or str(decoded.get("description") or ""),
150
+ destination=dest,
151
+ )
152
+ except Exception as e:
153
+ return PayerDecisionInputs(
154
+ payment_request=q.payment_request,
155
+ amount_sats=q.amount_sats,
156
+ total_cost_sats=q.total_cost_sats,
157
+ routing_fee_limit_sats=int(routing_fee_limit_sats),
158
+ quote_valid=False,
159
+ validation_error=str(e),
160
+ memo=q.memo,
161
+ )
162
+
163
+ def pay_invoice(
164
+ self, payment_request: str, fee_limit_sats: int = 200
165
+ ) -> PaymentResult:
166
+ if not payment_request:
167
+ raise ValueError("Payment request is required")
168
+ if not autopay_allowed():
169
+ raise RuntimeError(
170
+ "Lightning pay blocked: set AGENT_BITCOIN_ALLOW_AUTOPAY=1 "
171
+ "(required on mainnet; optional kill-switch on lab nets with =0). "
172
+ "See docs/mainnet-pilot.md Phase 2."
173
+ )
174
+
175
+ amount_sats = 0
176
+ try:
177
+ decoded = self.lnd.decode_pay_req(payment_request.strip())
178
+ amount_sats = _invoice_amount_sats(decoded)
179
+ except Exception:
180
+ decoded = {}
181
+
182
+ if amount_sats > 0:
183
+ if amount_sats < self.min_payment_sats:
184
+ raise ValueError(
185
+ f"Invoice amount {amount_sats} below minimum {self.min_payment_sats}"
186
+ )
187
+ if amount_sats > self.max_payment_sats:
188
+ raise ValueError(
189
+ f"Invoice amount {amount_sats} above maximum {self.max_payment_sats}"
190
+ )
191
+ assert_can_spend(amount_sats, self.max_daily_payment_sats)
192
+
193
+ result = self.lnd.pay_invoice(
194
+ payment_request.strip(), fee_limit_sats=fee_limit_sats
195
+ )
196
+ if result.success:
197
+ recorded = amount_sats or int(result.amount or 0)
198
+ if recorded > 0:
199
+ record_spend(recorded, payment_hash=result.payment_hash)
200
+ return result
201
+
202
+ def pay_invoice_quote(
203
+ self,
204
+ quote: InvoiceQuote | dict,
205
+ routing_fee_limit_sats: int = 200,
206
+ ) -> PaymentResult:
207
+ """
208
+ Payer: validate quote, budget total_cost_sats, pay BOLT11 amount on LN.
209
+
210
+ total_cost_sats equals the requested amount (no platform fee).
211
+ Daily/deposit ledger uses total_cost_sats.
212
+ """
213
+ q = self.validate_invoice_quote(quote)
214
+ if not autopay_allowed():
215
+ raise RuntimeError(
216
+ "Lightning pay blocked: set AGENT_BITCOIN_ALLOW_AUTOPAY=1 "
217
+ "(required on mainnet; optional kill-switch on lab nets with =0). "
218
+ "See docs/mainnet-pilot.md Phase 2."
219
+ )
220
+ if q.amount_sats < self.min_payment_sats:
221
+ raise ValueError(
222
+ f"Invoice amount {q.amount_sats} below minimum {self.min_payment_sats}"
223
+ )
224
+ if q.amount_sats > self.max_payment_sats:
225
+ raise ValueError(
226
+ f"Invoice amount {q.amount_sats} above maximum {self.max_payment_sats}"
227
+ )
228
+ assert_can_spend(q.total_cost_sats, self.max_daily_payment_sats)
229
+
230
+ result = self.lnd.pay_invoice(
231
+ q.payment_request.strip(), fee_limit_sats=routing_fee_limit_sats
232
+ )
233
+ if result.success:
234
+ record_spend(q.total_cost_sats, payment_hash=result.payment_hash)
235
+ return result
236
+
237
+ def send_onchain(self, address: str, amount_sats: int) -> OnChainSendResult:
238
+ if not fee_send_allowed():
239
+ raise RuntimeError(
240
+ "On-chain fee/send blocked on mainnet unless "
241
+ "AGENT_BITCOIN_ALLOW_MAINNET_FEE=1 (pilot default off)."
242
+ )
243
+ if not address:
244
+ raise ValueError("Destination address is required")
245
+ if amount_sats <= 0:
246
+ raise ValueError("Amount must be positive")
247
+ return self.lnd.send_coins(address, amount_sats)
248
+
249
+ def get_balance(self) -> LightningBalance:
250
+ return self.lnd.get_balance()
251
+
252
+ def get_channel_balance(self) -> ChannelBalance:
253
+ return self.lnd.get_channel_balance()
254
+
255
+
256
+ def create_client() -> AgentBitcoinClient:
257
+ """Factory function"""
258
+ return AgentBitcoinClient()
@@ -0,0 +1,116 @@
1
+ """
2
+ Shared payment policy defaults for SDK, backend API, and agents.
3
+
4
+ Env vars may still override at runtime; these are the single source of truth
5
+ for default numeric limits so invoice and agent ceilings cannot drift.
6
+
7
+ Mainnet pilot defaults (docs/mainnet-pilot.md Phase 0):
8
+ single pay 50_000, daily 100_000, autopay off, fee sends off.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+
15
+ # --- Canonical defaults (sats) ---
16
+ DEFAULT_MIN_PAYMENT_SATS = 1_000
17
+ DEFAULT_MAX_PAYMENT_SATS = 1_000_000
18
+ # When LND_NETWORK=mainnet and MAX_PAYMENT_SATS unset (pilot ceiling)
19
+ DEFAULT_MAINNET_MAX_PAYMENT_SATS = 50_000
20
+ DEFAULT_MAX_DAILY_PAYMENT_SATS = 100_000
21
+ DEFAULT_MAX_FEE_SEND_SATS = 100_000
22
+ # Aperture L402 price per paid request (matches MIN_PAYMENT_SATS)
23
+ DEFAULT_L402_PRICE_SATS = 1_000
24
+
25
+
26
+ def env_int(name: str, default: int) -> int:
27
+ """Read a positive-capable int from the environment, or return default."""
28
+ raw = os.getenv(name)
29
+ if raw is None or str(raw).strip() == "":
30
+ return default
31
+ return int(raw)
32
+
33
+
34
+ def current_network() -> str:
35
+ return os.getenv("LND_NETWORK", "regtest").strip().lower() or "regtest"
36
+
37
+
38
+ def is_mainnet() -> bool:
39
+ return current_network() == "mainnet"
40
+
41
+
42
+ def min_payment_sats() -> int:
43
+ return env_int("MIN_PAYMENT_SATS", DEFAULT_MIN_PAYMENT_SATS)
44
+
45
+
46
+ def max_payment_sats() -> int:
47
+ """
48
+ Maximum invoice / payment amount (sats).
49
+
50
+ MAX_INVOICE_SATS and PAYMENT_DECISION_MAX_SATS both default to this value.
51
+ On mainnet, default ceiling is pilot 50_000 unless MAX_PAYMENT_SATS is set.
52
+ """
53
+ if os.getenv("MAX_PAYMENT_SATS", "").strip():
54
+ return env_int("MAX_PAYMENT_SATS", DEFAULT_MAX_PAYMENT_SATS)
55
+ if is_mainnet():
56
+ return DEFAULT_MAINNET_MAX_PAYMENT_SATS
57
+ return DEFAULT_MAX_PAYMENT_SATS
58
+
59
+
60
+ def max_daily_payment_sats() -> int:
61
+ """
62
+ Max sum of successful Lightning pays per UTC day (sats).
63
+
64
+ 0 = disabled (no daily cap). On mainnet defaults to pilot 100_000;
65
+ on lab nets defaults to 0 (unlimited) unless MAX_DAILY_PAYMENT_SATS is set.
66
+ """
67
+ if os.getenv("MAX_DAILY_PAYMENT_SATS", "").strip():
68
+ return env_int("MAX_DAILY_PAYMENT_SATS", 0)
69
+ if is_mainnet():
70
+ return DEFAULT_MAX_DAILY_PAYMENT_SATS
71
+ return 0
72
+
73
+
74
+ def max_invoice_sats() -> int:
75
+ """Backend invoice ceiling; defaults to shared max payment."""
76
+ if os.getenv("MAX_INVOICE_SATS", "").strip():
77
+ return env_int("MAX_INVOICE_SATS", DEFAULT_MAX_PAYMENT_SATS)
78
+ return max_payment_sats()
79
+
80
+
81
+ def payment_decision_max_sats() -> int:
82
+ """Agent hard max; defaults to shared max payment."""
83
+ if os.getenv("PAYMENT_DECISION_MAX_SATS", "").strip():
84
+ return env_int("PAYMENT_DECISION_MAX_SATS", DEFAULT_MAX_PAYMENT_SATS)
85
+ return max_payment_sats()
86
+
87
+
88
+ def max_fee_send_sats() -> int:
89
+ return env_int("MAX_FEE_SEND_SATS", DEFAULT_MAX_FEE_SEND_SATS)
90
+
91
+
92
+ def autopay_allowed() -> bool:
93
+ """
94
+ Whether non-interactive Lightning pay is allowed.
95
+
96
+ Mainnet: require AGENT_BITCOIN_ALLOW_AUTOPAY=1 (human pilot default off).
97
+ Lab nets: allowed unless AGENT_BITCOIN_ALLOW_AUTOPAY=0.
98
+ """
99
+ flag = (os.getenv("AGENT_BITCOIN_ALLOW_AUTOPAY") or "").strip()
100
+ if is_mainnet():
101
+ return flag == "1"
102
+ if flag == "0":
103
+ return False
104
+ return True
105
+
106
+
107
+ def fee_send_allowed() -> bool:
108
+ """
109
+ Generic on-chain send (`send_onchain`).
110
+
111
+ Mainnet: disabled unless AGENT_BITCOIN_ALLOW_MAINNET_FEE=1.
112
+ Lab nets: allowed.
113
+ """
114
+ if is_mainnet():
115
+ return (os.getenv("AGENT_BITCOIN_ALLOW_MAINNET_FEE") or "").strip() == "1"
116
+ return True