diff --git a/.env.example b/.env.example index 44782248..4a9b3123 100644 --- a/.env.example +++ b/.env.example @@ -24,13 +24,13 @@ MODEL_BASED_PRICING = "true" # EXCHANGE_FEE = "1.005" # 0.5 % currency exchange fee # password used to log into admin interface -ADMIN_PASSWORD="CHANGE-THIS" +# ADMIN_PASSWORD="" # Public Endpoint HTTP_URL="https://your.domain.com" # Tor Endpoint (copy from docker logs) -ONION_URL=".onion" +# ONION_URL=".onion" RELAYS="wss://relay.routstr.com,wss://relay.nostr.band" CASHU_MINTS="https://mint.minibits.cash/Bitcoin,https://mint.cubabitcoin.org" diff --git a/.gitignore b/.gitignore index c21c4b2d..38976d5a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ wallet.sqlite3 .*wallet.sqlite3 *models.json .cashu +.dockerignore compose.override.yml diff --git a/README.md b/README.md index e4d60e14..b452bacc 100644 --- a/README.md +++ b/README.md @@ -32,22 +32,32 @@ sequenceDiagram - **Cashu Wallet Integration** – Accept Lightning payments and redeem eCash tokens before forwarding requests - **API Key Management** – Hashed keys stored in SQLite with balance tracking and optional expiry/refund address - **Model-Based Pricing** – Convert USD prices in `models.json` to sats using live BTC/USD rates -- **Admin Dashboard** – Simple HTML interface at `/admin` to view balances and API keys +- **Admin Dashboard** – Simple HTML interface at `/admin/` to view balances and API keys - **Discovery** – Fetch available providers from Nostr relays - **Docker Support** – Provided `Dockerfile` and `compose.yml` for running with an optional Tor hidden service ## Getting Started -### Requirements +### Running the proxy using Docker + +```bash +docker run -d \ +--name routstr-proxy \ +-p 8000:8000 \ +-e UPSTREAM_BASE_URL=https://api.openai.com/v1 \ +-e UPSTREAM_API_KEY=your-openai-api-key \ +ghcr.io/routstr/proxy:latest +``` + +### Development Requirements - Python 3.11+ - [uv](https://github.com/astral-sh/uv) package manager (used in development) -- A Cashu wallet secret (`NSEC`) and Lightning address for receiving payments ### Installation ```bash -uv sync --dev # install dependencies +uv sync # install dependencies ``` Create a `.env` file based on `.env.example` and fill in the required values: @@ -78,15 +88,18 @@ The most common settings are shown below. See `.env.example` for the full list. - `UPSTREAM_BASE_URL` – URL of the OpenAI-compatible service - `UPSTREAM_API_KEY` – API key for the upstream service (optional) -- `RECEIVE_LN_ADDRESS` – Lightning address that receives payouts -- `MINIMUM_PAYOUT` – Minimum sats before forwarding earnings - `MODEL_BASED_PRICING` – Set to `true` to use pricing from `models.json` -- `REFUND_PROCESSING_INTERVAL` – Seconds between automatic refunds -- `ADMIN_PASSWORD` – Password for the `/admin` dashboard +- `ADMIN_PASSWORD` – Password for the `/admin/` dashboard +- `CASHU_MINTS` – Comma-separated list of Cashu mint URLs +- `NAME` – Name of the proxy +- `DESCRIPTION` – Description of the proxy +- `NPUB` – Nostr public key of the proxy +- `HTTP_URL` – Public-facing URL of the proxy +- `ONION_URL` – Tor hidden service URL of the proxy ## Withdrawing Balance -Go to `https:///admin/` (NOTE: be sure to add the '/' at the end), enter the `ADMIN_PASSWORD` you set above and withdraw your balance as a Cashu token. +Go to `https:///admin/` (NOTE: be sure to add the '/' at the end), enter the `ADMIN_PASSWORD` you set above and withdraw your balance as a Cashu token. ## Example Client @@ -147,7 +160,8 @@ The proxy should implement either a dedicated endpoint to communicate minimum eC To use this feature, you'll need a client that handles both OpenAI API calls and eCash header management. The following clients provide seamless integration: -- **[cashu-402-client](https://github.com/9qeklajc/ecash-402-client)** – rust client with automatic wallet management +- **[routstr-chat](https://github.com/routstr/routstr-chat)** – chat app for the routstr network +- **[otrta-client](https://github.com/routstr/otrta-client)** – rust web app for the routstr network clients automatically: diff --git a/router/core/logging.py b/router/core/logging.py index b6217cc9..9820b747 100644 --- a/router/core/logging.py +++ b/router/core/logging.py @@ -276,6 +276,7 @@ def setup_logging() -> None: "propagate": False, }, "watchfiles.main": {"level": "WARNING", "handlers": [], "propagate": False}, + "aiosqlite": {"level": "ERROR", "handlers": [], "propagate": False}, }, "root": { "level": log_level, diff --git a/router/core/main.py b/router/core/main.py index b2130483..f4423936 100644 --- a/router/core/main.py +++ b/router/core/main.py @@ -10,7 +10,7 @@ from ..balance import balance_router, deprecated_wallet_router from ..discovery import providers_router from ..payment.models import MODELS, models_router, update_sats_pricing from ..proxy import proxy_router -from ..wallet import check_for_refunds, periodic_payout +from ..wallet import periodic_payout from .admin import admin_router from .db import init_db from .logging import get_logger, setup_logging @@ -19,7 +19,7 @@ from .logging import get_logger, setup_logging setup_logging() logger = get_logger(__name__) -__version__ = "0.0.1" +__version__ = "0.1.0" @asynccontextmanager @@ -30,7 +30,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: await init_db() pricing_task = asyncio.create_task(update_sats_pricing()) - refund_task = asyncio.create_task(check_for_refunds()) payout_task = asyncio.create_task(periodic_payout()) yield @@ -44,14 +43,11 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: finally: logger.info("Application shutdown initiated") - refund_task.cancel() pricing_task.cancel() payout_task.cancel() try: - await asyncio.gather( - pricing_task, refund_task, payout_task, return_exceptions=True - ) + await asyncio.gather(pricing_task, payout_task, return_exceptions=True) logger.info("Background tasks stopped successfully") except Exception as e: logger.error( diff --git a/router/payment/helpers.py b/router/payment/helpers.py index 6b58882b..61b8120a 100644 --- a/router/payment/helpers.py +++ b/router/payment/helpers.py @@ -10,17 +10,12 @@ from .models import MODELS logger = get_logger(__name__) -UPSTREAM_BASE_URL = os.environ["UPSTREAM_BASE_URL"] + +UPSTREAM_BASE_URL = os.environ.get("UPSTREAM_BASE_URL", "") UPSTREAM_API_KEY = os.environ.get("UPSTREAM_API_KEY", "") -logger.info( - "Payment helpers initialized", - extra={ - "upstream_base_url": UPSTREAM_BASE_URL, - "has_upstream_api_key": bool(UPSTREAM_API_KEY), - "model_based_pricing": MODEL_BASED_PRICING, - }, -) +if not UPSTREAM_BASE_URL: + raise ValueError("Please set the UPSTREAM_BASE_URL environment variable") def get_cost_per_request(model: str | None = None) -> int: diff --git a/router/payment/price.py b/router/payment/price.py index 4f0f4c88..27a26e9d 100644 --- a/router/payment/price.py +++ b/router/payment/price.py @@ -18,7 +18,6 @@ async def kraken_btc_usd(client: httpx.AsyncClient) -> float | None: """Fetch BTC/USD price from Kraken API.""" api = "https://api.kraken.com/0/public/Ticker?pair=XBTUSD" try: - logger.debug("Fetching BTC price from Kraken") response = await client.get(api) price_data = response.json() price = float(price_data["result"]["XXBTZUSD"]["c"][0]) @@ -40,7 +39,6 @@ async def coinbase_btc_usd(client: httpx.AsyncClient) -> float | None: """Fetch BTC/USD price from Coinbase API.""" api = "https://api.coinbase.com/v2/prices/BTC-USD/spot" try: - logger.debug("Fetching BTC price from Coinbase") response = await client.get(api) price_data = response.json() price = float(price_data["data"]["amount"]) @@ -62,7 +60,6 @@ async def binance_btc_usdt(client: httpx.AsyncClient) -> float | None: """Fetch BTC/USDT price from Binance API.""" api = "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT" try: - logger.debug("Fetching BTC price from Binance") response = await client.get(api) price_data = response.json() price = float(price_data["price"]) @@ -82,7 +79,6 @@ async def binance_btc_usdt(client: httpx.AsyncClient) -> float | None: async def btc_usd_ask_price() -> float: """Get the highest BTC/USD price from multiple exchanges with fee adjustment.""" - logger.debug("Starting BTC price aggregation from multiple exchanges") async with httpx.AsyncClient(timeout=30.0) as client: try: @@ -113,7 +109,6 @@ async def btc_usd_ask_price() -> float: async def sats_usd_ask_price() -> float: """Get the USD price per satoshi.""" - logger.debug("Calculating satoshi price from BTC price") try: btc_price = await btc_usd_ask_price() diff --git a/router/wallet.py b/router/wallet.py index 95cadfec..d41dd585 100644 --- a/router/wallet.py +++ b/router/wallet.py @@ -11,7 +11,8 @@ logger = get_logger(__name__) CurrencyUnit = Literal["sat", "msat"] -TRUSTED_MINTS = os.environ["CASHU_MINTS"].split(",") +CASHU_MINTS = os.environ.get("CASHU_MINTS", "https://mint.minibits.cash/Bitcoin") +TRUSTED_MINTS = CASHU_MINTS.split(",") PRIMARY_MINT_URL = TRUSTED_MINTS[0] @@ -122,10 +123,6 @@ async def send_to_lnurl(amount: int, unit: CurrencyUnit, lnurl: str) -> dict[str raise NotImplementedError -async def check_for_refunds() -> None: - logger.warning("check_for_refunds, temporary not implemented") - - async def periodic_payout() -> None: logger.warning("periodic_payout, temporary not implemented") diff --git a/uv.lock b/uv.lock index b1fb8dc5..3f9f591a 100644 --- a/uv.lock +++ b/uv.lock @@ -1323,7 +1323,7 @@ wheels = [ [[package]] name = "routstr" -version = "0.0.1" +version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "aiosqlite" },