Download splitbit_llm/subscription.py from hermescures1/splitbit-llm: direct link, hf CLI and curl.
- Browser
- Download file 31.9 kB
-
https://huggingface.co/hermescures1/splitbit-llm/resolve/main/splitbit_llm/subscription.py
- Command line
-
hf download hf://hermescures1/splitbit-llm/splitbit_llm/subscription.py
-
curl -L -o subscription.py https://huggingface.co/hermescures1/splitbit-llm/resolve/main/splitbit_llm/subscription.py
31.9 kB
| """Subscription Manager — $1.00/month subscription to use SplitBit LLM. | |
| Payments are routed through the Soulmate OS wallet system to the | |
| founder's wallet (hawpetossjustin25@gmail.com). Users can pay via: | |
| - Crypto (USDT, USDC, BNB, INC) on BSC to the founder wallet | |
| - Google Pay / card via the Soulmate OS wallet UI | |
| Payment flow: | |
| 1. User runs `splitbit-llm subscribe` to get payment instructions | |
| 2. User sends $1 worth of crypto to founder wallet, or pays via Soulmate OS UI | |
| 3. Payment is verified via Soulmate OS API (deposit status check) | |
| 4. On confirmation, subscription is activated for 30 days | |
| 5. Auto-renews unless cancelled | |
| 6. If expired, 3-day grace period before access is blocked | |
| 7. 7-day free trial on first run (no payment required) | |
| CLI commands: | |
| splitbit-llm subscribe — get payment instructions + activate | |
| splitbit-llm unsubscribe — cancel subscription | |
| splitbit-llm trial — start free trial | |
| splitbit-llm status — show subscription status | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import logging | |
| import os | |
| import time | |
| import threading | |
| import urllib.request | |
| import urllib.error | |
| import urllib.parse | |
| from typing import Any | |
| logger = logging.getLogger(__name__) | |
| MONTHLY_PRICE = 1.00 # $1.00/month | |
| SUBSCRIPTION_DURATION_DAYS = 30 | |
| TRIAL_DURATION_DAYS = 7 | |
| GRACE_PERIOD_DAYS = 3 | |
| # Soulmate OS wallet integration | |
| SOULMATE_API_URL = "https://191.44.121.29.sslip.io" | |
| SOULMATE_API_TOKEN = "soulmate_wallet_2024" | |
| FOUNDER_EMAIL = "hawpetossjustin25@gmail.com" | |
| FOUNDER_PASSWORD = "hawpetossjustin25@gmail.com15357979" | |
| ACCEPTED_TOKENS = ["USDT", "USDC", "BNB", "INC"] | |
| NETWORK = "BSC (Binance Smart Chain)" | |
| # Auto-transfer config — routes $1 payments to founder bank account | |
| # Bank routing/account from env vars (same pattern as Soulmate OS security model) | |
| BANK_ROUTING = os.environ.get("INC_LLM_CURRENT_ROUTING", "") | |
| BANK_ACCOUNT = os.environ.get("INC_LLM_CURRENT_ACCOUNT", "") | |
| AUTO_TRANSFER_ENABLED = True | |
| AUTO_TRANSFER_INTERVAL_S = 60 # check every 60 seconds | |
| class SubscriptionManager: | |
| """Manages $1/month subscription for SplitBit LLM access. | |
| Payments routed through Soulmate OS wallet to founder wallet. | |
| Features: | |
| - 7-day free trial on first run | |
| - $1.00/month subscription via crypto or Google Pay | |
| - 3-day grace period after expiration | |
| - Persistent state in subscription.json | |
| - Payment history tracking | |
| - Auto-renewal support | |
| - Soulmate OS wallet API integration | |
| - Crypto payment (USDT, USDC, BNB, INC on BSC) | |
| - Founder bypass — enter founder password for free forever access | |
| """ | |
| MONTHLY_PRICE = MONTHLY_PRICE | |
| SUBSCRIPTION_DURATION_DAYS = SUBSCRIPTION_DURATION_DAYS | |
| TRIAL_DURATION_DAYS = TRIAL_DURATION_DAYS | |
| GRACE_PERIOD_DAYS = GRACE_PERIOD_DAYS | |
| FOUNDER_PASSWORD = FOUNDER_PASSWORD | |
| def __init__(self, data_dir: str) -> None: | |
| self.data_dir = data_dir | |
| os.makedirs(data_dir, exist_ok=True) | |
| self.config_path = os.path.join(data_dir, "subscription.json") | |
| self._state = self._load_state() | |
| self._founder_wallet: str = "" | |
| self._wallet_fetched_at: float = 0.0 | |
| def _api_headers(self) -> dict[str, str]: | |
| return { | |
| "Content-Type": "application/json", | |
| "X-API-Token": SOULMATE_API_TOKEN, | |
| } | |
| def get_founder_wallet(self) -> str: | |
| """Fetch founder's BSC wallet address from Soulmate OS API. | |
| Caches result for 1 hour. Falls back to stored address if API is down. | |
| """ | |
| if self._founder_wallet and (time.time() - self._wallet_fetched_at) < 3600: | |
| return self._founder_wallet | |
| # Check if we have it stored in state | |
| if self._state.get("founder_wallet"): | |
| self._founder_wallet = self._state["founder_wallet"] | |
| self._wallet_fetched_at = time.time() | |
| return self._founder_wallet | |
| try: | |
| url = f"{SOULMATE_API_URL}/v1/users/wallet?email={urllib.parse.quote(FOUNDER_EMAIL)}" | |
| req = urllib.request.Request(url, headers=self._api_headers(), method="GET") | |
| resp = urllib.request.urlopen(req, timeout=15) | |
| data = json.loads(resp.read().decode()) | |
| wallet = data.get("wallet_address", "") | |
| if wallet: | |
| self._founder_wallet = wallet | |
| self._wallet_fetched_at = time.time() | |
| self._state["founder_wallet"] = wallet | |
| self._save_state() | |
| logger.info("Fetched founder wallet: %s", wallet[:10] + "...") | |
| return wallet | |
| except Exception as e: | |
| logger.warning("Failed to fetch founder wallet from Soulmate OS: %s", e) | |
| return self._founder_wallet | |
| def _load_state(self) -> dict[str, Any]: | |
| """Load subscription state from disk.""" | |
| if os.path.exists(self.config_path): | |
| try: | |
| with open(self.config_path, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except Exception as e: | |
| logger.warning("Failed to load subscription state: %s", e) | |
| return { | |
| "status": "none", # none, trial, active, expired, cancelled, grace | |
| "start_date": 0.0, | |
| "expiration_date": 0.0, | |
| "trial_start": 0.0, | |
| "trial_expiration": 0.0, | |
| "payment_history": [], | |
| "auto_renew": False, | |
| "total_paid": 0.0, | |
| "months_subscribed": 0, | |
| } | |
| def _save_state(self) -> None: | |
| """Save subscription state to disk.""" | |
| try: | |
| with open(self.config_path, "w", encoding="utf-8") as f: | |
| json.dump(self._state, f, indent=2) | |
| except Exception as e: | |
| logger.warning("Failed to save subscription state: %s", e) | |
| def founder_unlock(self, password: str) -> dict[str, Any]: | |
| """Unlock free forever access for the founder. | |
| Args: | |
| password: The founder password (email + code) | |
| Returns: | |
| Success dict with founder access status. | |
| """ | |
| if password != FOUNDER_PASSWORD: | |
| return { | |
| "success": False, | |
| "error": "Invalid founder password", | |
| } | |
| now = time.time() | |
| self._state["status"] = "founder" | |
| self._state["start_date"] = now | |
| self._state["expiration_date"] = float("inf") # forever | |
| self._state["auto_renew"] = False # no need to renew | |
| self._state["is_founder"] = True | |
| self._save_state() | |
| logger.info("Founder unlocked — free forever access") | |
| return { | |
| "success": True, | |
| "status": "founder", | |
| "message": "Founder access unlocked — free forever. Welcome back, Justin.", | |
| } | |
| def start_trial(self) -> dict[str, Any]: | |
| """Start a 7-day free trial. | |
| Only available if no previous subscription or trial exists. | |
| """ | |
| if self._state["status"] in ("trial", "active", "grace"): | |
| return {"success": False, "error": "Trial or subscription already active"} | |
| if self._state.get("trial_start", 0) > 0: | |
| return {"success": False, "error": "Trial already used"} | |
| now = time.time() | |
| self._state["status"] = "trial" | |
| self._state["trial_start"] = now | |
| self._state["trial_expiration"] = now + (TRIAL_DURATION_DAYS * 86400) | |
| self._save_state() | |
| logger.info("Free trial started — %d days", TRIAL_DURATION_DAYS) | |
| return { | |
| "success": True, | |
| "status": "trial", | |
| "start_date": now, | |
| "expiration_date": self._state["trial_expiration"], | |
| "days_remaining": TRIAL_DURATION_DAYS, | |
| "message": f"Free trial activated! {TRIAL_DURATION_DAYS} days remaining.", | |
| } | |
| def get_payment_instructions(self) -> dict[str, Any]: | |
| """Get payment instructions for the user. | |
| Returns founder wallet address and accepted tokens so the user | |
| can send crypto, plus a link to Soulmate OS wallet for Google Pay / card. | |
| """ | |
| wallet = self.get_founder_wallet() | |
| return { | |
| "amount": MONTHLY_PRICE, | |
| "currency": "USD", | |
| "period": "monthly", | |
| "founder_wallet": wallet or "Fetching from Soulmate OS...", | |
| "accepted_tokens": ACCEPTED_TOKENS, | |
| "network": NETWORK, | |
| "soulmate_wallet_url": f"{SOULMATE_API_URL}/#/wallet", | |
| "instructions": ( | |
| f"Send ${MONTHLY_PRICE:.2f} worth of {', '.join(ACCEPTED_TOKENS)} " | |
| f"to wallet: {wallet} on {NETWORK}. " | |
| f"Or pay via Google Pay / card at {SOULMATE_API_URL}/#/wallet" | |
| ), | |
| } | |
| def create_deposit(self, user_id: str, token: str = "USDT") -> dict[str, Any]: | |
| """Create a deposit request on Soulmate OS for the founder's wallet. | |
| This records a pending payment that the user needs to send crypto to. | |
| """ | |
| wallet = self.get_founder_wallet() | |
| if not wallet: | |
| return {"status": "error", "message": "Could not determine founder wallet address"} | |
| deposit_id = hashlib.sha256(f"{user_id}:{MONTHLY_PRICE}:{time.time()}".encode()).hexdigest()[:16] | |
| try: | |
| url = f"{SOULMATE_API_URL}/v1/wallet/deposit" | |
| payload = json.dumps({ | |
| "deposit_id": deposit_id, | |
| "user_id": user_id, | |
| "wallet_address": wallet, | |
| "amount": MONTHLY_PRICE, | |
| "token": token, | |
| "method": "crypto", | |
| "source": "splitbit-llm", | |
| }).encode() | |
| req = urllib.request.Request(url, data=payload, headers=self._api_headers(), method="POST") | |
| resp = urllib.request.urlopen(req, timeout=15) | |
| result = json.loads(resp.read().decode()) | |
| result["deposit_id"] = deposit_id | |
| result["founder_wallet"] = wallet | |
| result["token"] = token | |
| logger.info("Created deposit %s for user %s", deposit_id, user_id) | |
| return result | |
| except Exception as e: | |
| logger.warning("Deposit creation via API failed: %s", e) | |
| return { | |
| "status": "pending", | |
| "deposit_id": deposit_id, | |
| "founder_wallet": wallet, | |
| "token": token, | |
| "amount": MONTHLY_PRICE, | |
| "message": f"Send {MONTHLY_PRICE} {token} to {wallet}. Payment will be verified manually.", | |
| } | |
| def verify_payment(self, deposit_id: str) -> dict[str, Any]: | |
| """Verify a payment by checking deposit status on Soulmate OS API. | |
| Returns: | |
| {"status": "confirmed", ...} on success | |
| {"status": "pending"} if still pending | |
| {"status": "not_found"} if deposit doesn't exist | |
| """ | |
| try: | |
| url = f"{SOULMATE_API_URL}/v1/wallet/deposit/{deposit_id}/status" | |
| req = urllib.request.Request(url, headers=self._api_headers(), method="GET") | |
| resp = urllib.request.urlopen(req, timeout=15) | |
| result = json.loads(resp.read().decode()) | |
| logger.info("Deposit %s status: %s", deposit_id, result.get("status")) | |
| return result | |
| except urllib.error.HTTPError as e: | |
| if e.code == 404: | |
| return {"status": "not_found", "message": "Deposit not found"} | |
| logger.error("Deposit verification HTTP error: %s", e) | |
| return {"status": "error", "message": f"HTTP {e.code}: {e.reason}"} | |
| except Exception as e: | |
| logger.error("Deposit verification failed: %s", e) | |
| return {"status": "error", "message": str(e)} | |
| def subscribe(self, payment_reference: str = "", token: str = "USDT", | |
| user_id: str = "", create_deposit: bool = True) -> dict[str, Any]: | |
| """Start or renew a $1.00/month subscription. | |
| If create_deposit is True, creates a deposit request on Soulmate OS | |
| and returns payment instructions. The subscription is activated | |
| immediately (payment verified later via verify_payment). | |
| Args: | |
| payment_reference: Payment transaction ID or deposit ID | |
| token: Crypto token for payment (USDT, USDC, BNB, INC) | |
| user_id: User identifier for deposit tracking | |
| create_deposit: If True, create a deposit on Soulmate OS API | |
| """ | |
| now = time.time() | |
| wallet = self.get_founder_wallet() | |
| deposit_id = "" | |
| # Create deposit on Soulmate OS if requested | |
| if create_deposit and user_id: | |
| deposit = self.create_deposit(user_id, token) | |
| deposit_id = deposit.get("deposit_id", "") | |
| if not payment_reference: | |
| payment_reference = deposit_id | |
| # Check if already active | |
| if self._state["status"] == "active" and now < self._state["expiration_date"]: | |
| # Extend by 30 days from current expiration | |
| self._state["expiration_date"] += SUBSCRIPTION_DURATION_DAYS * 86400 | |
| else: | |
| # New subscription or reactivation | |
| self._state["status"] = "active" | |
| self._state["start_date"] = now | |
| self._state["expiration_date"] = now + (SUBSCRIPTION_DURATION_DAYS * 86400) | |
| self._state["auto_renew"] = True | |
| self._state["months_subscribed"] += 1 | |
| self._state["total_paid"] += MONTHLY_PRICE | |
| # Record payment | |
| self._state["payment_history"].append({ | |
| "date": now, | |
| "amount": MONTHLY_PRICE, | |
| "reference": payment_reference or f"manual-{int(now)}", | |
| "type": "monthly", | |
| "token": token, | |
| "founder_wallet": wallet[:10] + "..." if wallet else "", | |
| "deposit_id": deposit_id, | |
| }) | |
| self._save_state() | |
| logger.info("Subscription activated — expires %s", | |
| time.strftime("%Y-%m-%d", time.localtime(self._state["expiration_date"]))) | |
| result = { | |
| "success": True, | |
| "status": "active", | |
| "start_date": self._state["start_date"], | |
| "expiration_date": self._state["expiration_date"], | |
| "days_remaining": self.get_days_remaining(), | |
| "price": MONTHLY_PRICE, | |
| "months_subscribed": self._state["months_subscribed"], | |
| "total_paid": self._state["total_paid"], | |
| "message": f"Subscription active! ${MONTHLY_PRICE:.2f}/month — {self.get_days_remaining()} days remaining.", | |
| } | |
| # Include payment instructions if deposit was created | |
| if deposit_id: | |
| result["payment"] = self.get_payment_instructions() | |
| result["deposit_id"] = deposit_id | |
| result["founder_wallet"] = wallet | |
| result["message"] = ( | |
| f"Subscription activated! Send ${MONTHLY_PRICE:.2f} worth of {token} " | |
| f"to {wallet[:10]}... on {NETWORK}. " | |
| f"Or pay at {SOULMATE_API_URL}/#/wallet" | |
| ) | |
| return result | |
| def unsubscribe(self) -> dict[str, Any]: | |
| """Cancel subscription (disables auto-renew).""" | |
| self._state["auto_renew"] = False | |
| self._state["status"] = "cancelled" | |
| self._save_state() | |
| logger.info("Subscription cancelled — access until expiration") | |
| return { | |
| "success": True, | |
| "status": "cancelled", | |
| "expiration_date": self._state["expiration_date"], | |
| "days_remaining": self.get_days_remaining(), | |
| "message": f"Subscription cancelled. Access continues until expiration ({self.get_days_remaining()} days).", | |
| } | |
| def check_access(self) -> dict[str, Any]: | |
| """Check if the user has access to the LLM. | |
| Returns dict with: | |
| - has_access: bool | |
| - status: current status | |
| - days_remaining: days until expiration | |
| - message: status message | |
| """ | |
| now = time.time() | |
| self._update_status(now) | |
| if self._state["status"] == "founder": | |
| return { | |
| "has_access": True, | |
| "status": "founder", | |
| "days_remaining": -1, # forever | |
| "message": "Founder access — free forever", | |
| } | |
| if self._state["status"] == "trial": | |
| days = max(0, int((self._state["trial_expiration"] - now) / 86400)) | |
| return { | |
| "has_access": True, | |
| "status": "trial", | |
| "days_remaining": days, | |
| "message": f"Free trial — {days} days remaining", | |
| } | |
| if self._state["status"] == "active": | |
| days = self.get_days_remaining() | |
| return { | |
| "has_access": True, | |
| "status": "active", | |
| "days_remaining": days, | |
| "message": f"Subscription active — {days} days remaining", | |
| } | |
| if self._state["status"] == "cancelled": | |
| days = self.get_days_remaining() | |
| if days > 0: | |
| return { | |
| "has_access": True, | |
| "status": "cancelled", | |
| "days_remaining": days, | |
| "message": f"Subscription cancelled — {days} days remaining until expiration", | |
| } | |
| # Fall through to expired | |
| if self._state["status"] == "grace": | |
| exp = self._state["expiration_date"] + (GRACE_PERIOD_DAYS * 86400) | |
| days = max(0, int((exp - now) / 86400)) | |
| return { | |
| "has_access": True, | |
| "status": "grace", | |
| "days_remaining": days, | |
| "message": f"Subscription expired — {days} days grace period remaining. Renew for ${MONTHLY_PRICE:.2f}/month.", | |
| } | |
| if self._state["status"] == "expired": | |
| return { | |
| "has_access": False, | |
| "status": "expired", | |
| "days_remaining": 0, | |
| "message": f"Subscription expired. Renew for ${MONTHLY_PRICE:.2f}/month to continue using SplitBit LLM.", | |
| } | |
| # No subscription at all — offer trial or founder unlock | |
| if self._state.get("trial_start", 0) == 0: | |
| return { | |
| "has_access": False, | |
| "status": "none", | |
| "days_remaining": 0, | |
| "message": f"No subscription. Start a {TRIAL_DURATION_DAYS}-day free trial, subscribe for ${MONTHLY_PRICE:.2f}/month, or enter founder password.", | |
| } | |
| # Trial was used, no active subscription | |
| return { | |
| "has_access": False, | |
| "status": "none", | |
| "days_remaining": 0, | |
| "message": f"Subscribe for ${MONTHLY_PRICE:.2f}/month or enter founder password to continue.", | |
| } | |
| def _update_status(self, now: float) -> None: | |
| """Update subscription status based on current time.""" | |
| if self._state["status"] == "trial": | |
| if now >= self._state["trial_expiration"]: | |
| self._state["status"] = "expired" | |
| self._save_state() | |
| elif self._state["status"] == "active": | |
| if now >= self._state["expiration_date"]: | |
| # Check if auto-renew should kick in | |
| if self._state["auto_renew"]: | |
| # Auto-renew: extend subscription | |
| self._state["expiration_date"] = now + (SUBSCRIPTION_DURATION_DAYS * 86400) | |
| self._state["months_subscribed"] += 1 | |
| self._state["total_paid"] += MONTHLY_PRICE | |
| self._state["payment_history"].append({ | |
| "date": now, | |
| "amount": MONTHLY_PRICE, | |
| "reference": f"auto-renew-{int(now)}", | |
| "type": "auto-renew", | |
| }) | |
| logger.info("Auto-renewed subscription") | |
| else: | |
| # Enter grace period | |
| self._state["status"] = "grace" | |
| self._save_state() | |
| elif self._state["status"] == "grace": | |
| grace_end = self._state["expiration_date"] + (GRACE_PERIOD_DAYS * 86400) | |
| if now >= grace_end: | |
| self._state["status"] = "expired" | |
| self._save_state() | |
| elif self._state["status"] == "cancelled": | |
| if now >= self._state["expiration_date"]: | |
| self._state["status"] = "expired" | |
| self._save_state() | |
| def get_days_remaining(self) -> int: | |
| """Get days remaining on current subscription.""" | |
| now = time.time() | |
| if self._state["status"] == "trial": | |
| return max(0, int((self._state["trial_expiration"] - now) / 86400)) | |
| elif self._state["status"] in ("active", "cancelled"): | |
| return max(0, int((self._state["expiration_date"] - now) / 86400)) | |
| elif self._state["status"] == "grace": | |
| grace_end = self._state["expiration_date"] + (GRACE_PERIOD_DAYS * 86400) | |
| return max(0, int((grace_end - now) / 86400)) | |
| return 0 | |
| def get_stats(self) -> dict[str, Any]: | |
| """Get subscription stats.""" | |
| now = time.time() | |
| self._update_status(now) | |
| return { | |
| "status": self._state["status"], | |
| "start_date": self._state["start_date"], | |
| "expiration_date": self._state["expiration_date"], | |
| "days_remaining": self.get_days_remaining(), | |
| "monthly_price": MONTHLY_PRICE, | |
| "auto_renew": self._state["auto_renew"], | |
| "months_subscribed": self._state["months_subscribed"], | |
| "total_paid": self._state["total_paid"], | |
| "trial_used": self._state.get("trial_start", 0) > 0, | |
| "trial_days_remaining": max(0, int((self._state.get("trial_expiration", 0) - now) / 86400)) | |
| if self._state["status"] == "trial" else 0, | |
| "payment_history_count": len(self._state["payment_history"]), | |
| "payment_history": self._state["payment_history"][-5:], # last 5 payments | |
| "grace_period_days": GRACE_PERIOD_DAYS, | |
| "trial_duration_days": TRIAL_DURATION_DAYS, | |
| } | |
| # ===== AUTO-TRANSFER SYSTEM ===== | |
| # Monitors for confirmed $1 subscription payments and automatically | |
| # transfers them from the founder wallet to the founder's bank account. | |
| # | |
| # Flow: | |
| # 1. Payment confirmed on Soulmate OS (deposit status = "confirmed") | |
| # 2. Auto-transfer checks for un-transferred payments | |
| # 3. For each confirmed payment, initiates a withdrawal via Soulmate OS API | |
| # 4. Withdrawal routes to founder bank account (routing + account number) | |
| # 5. Transfer recorded in auto_transfer_history | |
| def _init_auto_transfer(self) -> None: | |
| """Initialize auto-transfer state if not present.""" | |
| if "auto_transfer_history" not in self._state: | |
| self._state["auto_transfer_history"] = [] | |
| if "transferred_payments" not in self._state: | |
| self._state["transferred_payments"] = [] | |
| if "auto_transfer_enabled" not in self._state: | |
| self._state["auto_transfer_enabled"] = AUTO_TRANSFER_ENABLED | |
| self._save_state() | |
| def get_bank_info(self) -> dict[str, Any]: | |
| """Get founder bank account info (masked for safety).""" | |
| return { | |
| "routing_number": BANK_ROUTING[:4] + "****" if BANK_ROUTING else "Not set", | |
| "account_number": BANK_ACCOUNT[:4] + "****" if BANK_ACCOUNT else "Not set", | |
| "auto_transfer_enabled": self._state.get("auto_transfer_enabled", AUTO_TRANSFER_ENABLED), | |
| "total_transferred": sum(t["amount"] for t in self._state.get("auto_transfer_history", [])), | |
| "transfer_count": len(self._state.get("auto_transfer_history", [])), | |
| } | |
| def set_auto_transfer(self, enabled: bool) -> dict[str, Any]: | |
| """Enable or disable auto-transfer.""" | |
| self._init_auto_transfer() | |
| self._state["auto_transfer_enabled"] = enabled | |
| self._save_state() | |
| logger.info("Auto-transfer %s", "enabled" if enabled else "disabled") | |
| return { | |
| "success": True, | |
| "auto_transfer_enabled": enabled, | |
| "message": f"Auto-transfer {'enabled' if enabled else 'disabled'}", | |
| } | |
| def _get_untransferred_payments(self) -> list[dict[str, Any]]: | |
| """Get list of confirmed payments that haven't been transferred to bank yet.""" | |
| self._init_auto_transfer() | |
| transferred = set(self._state.get("transferred_payments", [])) | |
| untransferred = [] | |
| for payment in self._state.get("payment_history", []): | |
| ref = payment.get("reference", "") | |
| if ref and ref not in transferred: | |
| untransferred.append(payment) | |
| return untransferred | |
| def _initiate_bank_transfer(self, amount: float, payment_ref: str) -> dict[str, Any]: | |
| """Initiate a transfer from founder wallet to founder bank account. | |
| Uses the Soulmate OS API to: | |
| 1. Send crypto from founder wallet to an off-ramp address | |
| 2. Off-ramp converts crypto to USD and deposits to bank account | |
| 3. Bank account identified by routing + account number | |
| Falls back to recording the transfer as pending if API is unavailable. | |
| """ | |
| if not BANK_ROUTING or not BANK_ACCOUNT: | |
| logger.warning("Bank routing/account not set — transfer recorded as pending") | |
| return { | |
| "status": "pending", | |
| "amount": amount, | |
| "reference": payment_ref, | |
| "message": "Bank routing/account not configured. Set INC_LLM_CURRENT_ROUTING and INC_LLM_CURRENT_ACCOUNT env vars.", | |
| } | |
| transfer_id = hashlib.sha256(f"bank:{payment_ref}:{amount}:{time.time()}".encode()).hexdigest()[:16] | |
| try: | |
| # Call Soulmate OS API to initiate withdrawal to bank | |
| url = f"{SOULMATE_API_URL}/v1/wallet/withdraw" | |
| payload = json.dumps({ | |
| "transfer_id": transfer_id, | |
| "amount": amount, | |
| "currency": "USD", | |
| "routing_number": BANK_ROUTING, | |
| "account_number": BANK_ACCOUNT, | |
| "source": "splitbit-llm-auto-transfer", | |
| "payment_reference": payment_ref, | |
| }).encode() | |
| req = urllib.request.Request(url, data=payload, headers=self._api_headers(), method="POST") | |
| resp = urllib.request.urlopen(req, timeout=30) | |
| result = json.loads(resp.read().decode()) | |
| result["transfer_id"] = transfer_id | |
| logger.info("Bank transfer initiated: %s for $%.2f (ref: %s)", transfer_id, amount, payment_ref) | |
| return result | |
| except Exception as e: | |
| logger.warning("Bank transfer via API failed: %s — recorded as pending", e) | |
| return { | |
| "status": "pending", | |
| "transfer_id": transfer_id, | |
| "amount": amount, | |
| "reference": payment_ref, | |
| "routing_number": BANK_ROUTING[:4] + "****", | |
| "account_number": BANK_ACCOUNT[:4] + "****", | |
| "message": f"Transfer recorded as pending. Will retry. Bank: {BANK_ROUTING[:4]}**** / {BANK_ACCOUNT[:4]}****", | |
| } | |
| def process_auto_transfers(self) -> dict[str, Any]: | |
| """Process all un-transferred confirmed payments. | |
| For each payment that hasn't been transferred to the bank account yet: | |
| 1. Initiate a bank transfer via Soulmate OS API | |
| 2. Record the transfer in auto_transfer_history | |
| 3. Mark the payment as transferred | |
| Returns summary of transfers processed. | |
| """ | |
| self._init_auto_transfer() | |
| if not self._state.get("auto_transfer_enabled", AUTO_TRANSFER_ENABLED): | |
| return {"status": "disabled", "message": "Auto-transfer is disabled"} | |
| untransferred = self._get_untransferred_payments() | |
| if not untransferred: | |
| return { | |
| "status": "ok", | |
| "transfers_made": 0, | |
| "message": "No pending transfers — all payments routed to bank", | |
| } | |
| transfers = [] | |
| for payment in untransferred: | |
| amount = payment.get("amount", MONTHLY_PRICE) | |
| ref = payment.get("reference", "") | |
| # Initiate bank transfer | |
| result = self._initiate_bank_transfer(amount, ref) | |
| # Record transfer | |
| transfer_record = { | |
| "date": time.time(), | |
| "amount": amount, | |
| "payment_reference": ref, | |
| "transfer_id": result.get("transfer_id", ""), | |
| "status": result.get("status", "pending"), | |
| "routing_number": BANK_ROUTING[:4] + "****" if BANK_ROUTING else "", | |
| "account_number": BANK_ACCOUNT[:4] + "****" if BANK_ACCOUNT else "", | |
| } | |
| self._state["auto_transfer_history"].append(transfer_record) | |
| self._state["transferred_payments"].append(ref) | |
| transfers.append(transfer_record) | |
| logger.info("Auto-transferred $%.2f (ref: %s) → bank %s****/%s****", | |
| amount, ref, BANK_ROUTING[:4] if BANK_ROUTING else "????", | |
| BANK_ACCOUNT[:4] if BANK_ACCOUNT else "????") | |
| self._save_state() | |
| return { | |
| "status": "ok", | |
| "transfers_made": len(transfers), | |
| "total_amount": sum(t["amount"] for t in transfers), | |
| "transfers": transfers, | |
| "message": f"Processed {len(transfers)} transfer(s) totaling ${sum(t['amount'] for t in transfers):.2f} to bank account", | |
| } | |
| def get_auto_transfer_stats(self) -> dict[str, Any]: | |
| """Get auto-transfer statistics.""" | |
| self._init_auto_transfer() | |
| history = self._state.get("auto_transfer_history", []) | |
| return { | |
| "auto_transfer_enabled": self._state.get("auto_transfer_enabled", AUTO_TRANSFER_ENABLED), | |
| "bank_routing": BANK_ROUTING[:4] + "****" if BANK_ROUTING else "Not set", | |
| "bank_account": BANK_ACCOUNT[:4] + "****" if BANK_ACCOUNT else "Not set", | |
| "total_transferred": sum(t["amount"] for t in history), | |
| "transfer_count": len(history), | |
| "pending_transfers": len([t for t in history if t["status"] == "pending"]), | |
| "completed_transfers": len([t for t in history if t["status"] in ("completed", "confirmed", "ok")]), | |
| "untransferred_payments": len(self._get_untransferred_payments()), | |
| "recent_transfers": history[-5:], | |
| } | |
| def start_auto_transfer_monitor(self) -> threading.Thread: | |
| """Start a background thread that monitors for confirmed payments | |
| and auto-transfers them to the founder's bank account. | |
| Runs every AUTO_TRANSFER_INTERVAL_S seconds. | |
| """ | |
| self._init_auto_transfer() | |
| def _monitor_loop(): | |
| logger.info("Auto-transfer monitor started — checking every %ds", AUTO_TRANSFER_INTERVAL_S) | |
| while True: | |
| try: | |
| if self._state.get("auto_transfer_enabled", AUTO_TRANSFER_ENABLED): | |
| result = self.process_auto_transfers() | |
| if result.get("transfers_made", 0) > 0: | |
| logger.info("Auto-transfer: %s", result.get("message", "")) | |
| except Exception as e: | |
| logger.error("Auto-transfer monitor error: %s", e) | |
| time.sleep(AUTO_TRANSFER_INTERVAL_S) | |
| thread = threading.Thread(target=_monitor_loop, daemon=True, name="auto-transfer-monitor") | |
| thread.start() | |
| return thread | |