From 9c66789665f68243882302ba5825149507d0133e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Sat, 24 Jan 2026 17:55:45 +0800 Subject: [PATCH] improved docs --- docs/api/authentication.md | 2 +- docs/api/endpoints.md | 2 +- docs/api/errors.md | 2 +- docs/api/overview.md | 125 ++--- docs/client/integration.md | 143 ++++++ docs/client/introduction.md | 142 ++++++ docs/client/payments.md | 97 ++++ docs/contributing/architecture.md | 216 ++++----- docs/contributing/code-structure.md | 248 ++++------ docs/contributing/setup.md | 121 +++-- docs/contributing/testing.md | 688 +++++++--------------------- docs/index.md | 95 ++-- docs/overview.md | 76 +++ docs/provider/advanced-pricing.md | 114 +++++ docs/provider/configuration.md | 122 +++++ docs/provider/dashboard.md | 208 +++++++++ docs/provider/deployment.md | 196 ++++++++ docs/provider/discovery.md | 95 ++++ docs/provider/pricing.md | 91 ++++ docs/provider/quickstart.md | 99 ++++ docs/provider/tor.md | 50 ++ mkdocs.yml | 40 +- 22 files changed, 1948 insertions(+), 1024 deletions(-) create mode 100644 docs/client/integration.md create mode 100644 docs/client/introduction.md create mode 100644 docs/client/payments.md create mode 100644 docs/overview.md create mode 100644 docs/provider/advanced-pricing.md create mode 100644 docs/provider/configuration.md create mode 100644 docs/provider/dashboard.md create mode 100644 docs/provider/deployment.md create mode 100644 docs/provider/discovery.md create mode 100644 docs/provider/pricing.md create mode 100644 docs/provider/quickstart.md create mode 100644 docs/provider/tor.md diff --git a/docs/api/authentication.md b/docs/api/authentication.md index 566d130..c7b91b3 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -425,4 +425,4 @@ All API key usage is logged: - [Endpoints](endpoints.md) - Complete endpoint reference - [Errors](errors.md) - Error handling guide -- [Using the API](../user-guide/using-api.md) - Integration examples +- [Using the API](../client/integration.md) - Integration examples diff --git a/docs/api/endpoints.md b/docs/api/endpoints.md index abff6d6..2e9c6ab 100644 --- a/docs/api/endpoints.md +++ b/docs/api/endpoints.md @@ -527,4 +527,4 @@ Rate limit information is included in response headers. - [Errors](errors.md) - Error handling reference - [Authentication](authentication.md) - Auth details -- [Examples](../user-guide/using-api.md) - Code examples +- [Integration Guide](../client/integration.md) - Code examples diff --git a/docs/api/errors.md b/docs/api/errors.md index c94d04a..dc970cb 100644 --- a/docs/api/errors.md +++ b/docs/api/errors.md @@ -589,4 +589,4 @@ class ErrorMetrics: - [Authentication](authentication.md) - Auth error details - [Endpoints](endpoints.md) - Endpoint-specific errors -- [Examples](../user-guide/using-api.md) - Error handling examples +- [Integration Guide](../client/integration.md) - Error handling examples diff --git a/docs/api/overview.md b/docs/api/overview.md index 4849b5b..b563650 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -99,19 +99,19 @@ All errors follow a consistent format: Standard OpenAI-compatible endpoints: -- **Chat Completions**: `/v1/chat/completions` -- **Completions**: `/v1/completions` *(Coming soon)* -- **Embeddings**: `/v1/embeddings` *(Coming soon)* -- **Images**: `/v1/images/generations` *(Coming soon)* -- **Audio**: `/v1/audio/transcriptions` *(Coming soon)* - **Models**: `/v1/models` +- **Responses**: `/v1/responses` +- **Chat Completions**: `/v1/chat/completions` +- **Embeddings**: `/v1/embeddings` +- **Completions**: `/v1/completions` *(planned)* +- **Images**: `/v1/images/generations` *(planned)* +- **Audio**: `/v1/audio/transcriptions` *(planned)* ### Payment Endpoints Routstr-specific payment management: -- **Wallet**: `/v1/wallet/*` -- **Balance**: `/v1/balance` +- **Balance**: `/v1/balance/*` - **Node Info**: `/v1/info` ### Admin Endpoints @@ -137,9 +137,25 @@ Protected administrative functions: | Header | Description | |--------|-------------| -| `X-Routstr-Version` | API version override | | `X-Cashu` | eCash token for per-request payment | -| `X-Max-Cost` | Maximum acceptable cost in sats | + +#### X-Cashu: Stateless Per-Request Payment + +Instead of using `Authorization: Bearer sk-...`, you can send a Cashu token directly in the `X-Cashu` header. The response will include an `X-Cashu-Refund` header with your change. + +```bash +curl https://api.routstr.com/v1/chat/completions \ + -H "X-Cashu: cashuA3s8jKx9..." \ + -H "Content-Type: application/json" \ + -d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}' +``` + +The response includes your change in the same header: +``` +X-Cashu: cashuA7k2mNp4... +``` + +This is fully stateless—no session, no `/v1/balance/refund` call needed. However, **streaming does not work with `X-Cashu`** because the refund can only be calculated after the full response is generated. ## Response Headers @@ -149,16 +165,8 @@ Protected administrative functions: |--------|-------------| | `Content-Type` | Response format | | `Content-Length` | Response size | -| `X-Routstr-Request-ID` | Unique request identifier | -| `X-Routstr-Version` | API version used | - -### Cost Headers - -| Header | Description | -|--------|-------------| -| `X-Routstr-Cost` | Request cost in sats | -| `X-Routstr-Balance` | Remaining balance | -| `X-Cashu` | Change token (if applicable) | +| `X-Request-ID` | Unique request identifier | +| `X-Cashu` | Change token (when request used `X-Cashu` header) | ## Streaming Responses @@ -245,8 +253,6 @@ X-Webhook-Signature: sha256=... - Current version: `v1` - Version in URL path: `/v1/endpoint` -- Override with header: `X-Routstr-Version: v2` -- Deprecation notices: 6 months ## Status Codes @@ -284,82 +290,23 @@ Responses are compressed with gzip when: - Response is larger than 1KB - Content type is compressible -## Pagination +## Batch Requests *(planned)* -List endpoints support pagination: +Process multiple operations in one request. Coming soon. + +## Node Info + +Get node metadata: ``` -GET /v1/transactions?limit=50&offset=100 +GET /v1/info ``` -Response includes pagination metadata: - -```json -{ - "data": [...], - "has_more": true, - "total": 500, - "limit": 50, - "offset": 100 -} -``` - -## Field Filtering - -Select specific fields in responses: - -``` -GET /v1/models?fields=id,name,pricing -``` - -## Batch Requests - -Process multiple operations in one request: - -```json -POST /v1/batch -{ - "requests": [ - {"method": "POST", "endpoint": "/chat/completions", "body": {...}}, - {"method": "GET", "endpoint": "/models"}, - {"method": "GET", "endpoint": "/balance"} - ] -} -``` - -## Idempotency - -Prevent duplicate operations: - -``` -Idempotency-Key: unique-request-id -``` - -Keys are stored for 24 hours. - -## Health Check - -Monitor service status: - -``` -GET /health - -Response: -{ - "status": "healthy", - "version": "0.2.0", - "timestamp": "2024-01-01T00:00:00Z", - "checks": { - "database": "ok", - "upstream": "ok", - "mint": "ok" - } -} -``` +Supported models and pricing are available at `/v1/models`. ## Next Steps - [Authentication](authentication.md) - Detailed auth guide - [Endpoints](endpoints.md) - Complete endpoint reference - [Errors](errors.md) - Error handling guide -- [Examples](../user-guide/using-api.md) - Code examples +- [Integration Guide](../client/integration.md) - Code examples diff --git a/docs/client/integration.md b/docs/client/integration.md new file mode 100644 index 0000000..daf576a --- /dev/null +++ b/docs/client/integration.md @@ -0,0 +1,143 @@ +# Using the API + +Routstr is **OpenAI-compatible**. Almost any AI application, SDK, or tool that supports custom endpoints will work out of the box. Just change two things: + +``` +BASE_URL → https://api.routstr.com/v1 +API_KEY → sk-... or cashuA... +``` + +**Both work as API keys:** +- `sk-7f8e9d...` — Session key (from Lightning invoice or Cashu import) +- `cashuA3s8j...` — Raw Cashu token (use directly from your wallet) + +If the app lets you set a base URL and API key, you're good to go. + +--- + +## Quick Setup Examples + +### OpenAI SDK (Python/JS) + +```python +client = OpenAI(base_url="https://api.routstr.com/v1", api_key="sk-...") # or any provider's URL +``` + +### Claude Code + +```bash +export ANTHROPIC_BASE_URL=https://api.routstr.com/v1 +export ANTHROPIC_AUTH_TOKEN=sk-... +``` + +### Any OpenAI-compatible app + +Look for "Custom API endpoint", "Base URL", or "OpenAI-compatible" in settings. Paste the URL and key. + +--- + +## Detailed Examples + +### Python (Official SDK) + +```python +from openai import OpenAI + +# 1. Initialize with Routstr URL and your funded key +client = OpenAI( + base_url="https://api.routstr.com/v1", + api_key="sk-7f8e9d..." +) + +# 2. Call the API normally +response = client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello!"}], + stream=True +) + +for chunk in response: + if chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="") +``` + +### Node.js + +```javascript +import OpenAI from 'openai'; + +// You can use a session key OR a raw Cashu token directly +const openai = new OpenAI({ + baseURL: 'https://api.routstr.com/v1', + apiKey: 'cashuA3s8jKx9...', // or 'sk-7f8e9d...' +}); + +async function main() { + const completion = await openai.chat.completions.create({ + messages: [{ role: 'user', content: 'Say this is a test' }], + model: 'gpt-3.5-turbo', + }); + + console.log(completion.choices[0]); +} + +main(); +``` + +### cURL + +```bash +# Works with session key or raw Cashu token +curl https://api.routstr.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer cashuA3s8jKx9..." \ + -d '{ + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "Hello!"}] + }' +``` + +--- + +## Error Handling + +### Insufficient Balance (402 Payment Required) +If your session runs out of funds, the API will return a `402` error. + +```json +{ + "error": { + "message": "Insufficient balance. Current: 1000 msat, Required: 5000 msat", + "type": "insufficient_balance", + "code": 402 + } +} +``` + +**Action**: Top up your key using the `/lightning/invoice` (topup purpose) or `/v1/balance/topup` endpoints. + +### Rate Limiting +Routstr passes through rate limits from the upstream provider. Handle `429 Too Many Requests` with standard exponential backoff. + +--- + +## Advanced: Tor Access + +If the node is running as a hidden service, use a SOCKS5 proxy (like `127.0.0.1:9050`). + +**Python:** +```python +import httpx +from openai import OpenAI + +proxy_mounts = { + "http://": httpx.HTTPTransport(proxy="socks5://127.0.0.1:9050"), + "https://": httpx.HTTPTransport(proxy="socks5://127.0.0.1:9050"), +} + +client = OpenAI( + base_url="http://verylongonionaddress.onion/v1", + api_key="sk-...", + http_client=httpx.Client(mounts=proxy_mounts), +) +``` diff --git a/docs/client/introduction.md b/docs/client/introduction.md new file mode 100644 index 0000000..72479a1 --- /dev/null +++ b/docs/client/introduction.md @@ -0,0 +1,142 @@ +# Introduction to Routstr + +Welcome to the Routstr Core User Guide. This guide will help you understand how to use Routstr to access AI APIs with Bitcoin micropayments. + +## What You'll Learn + +- How the payment system works (Cashu eCash) +- Creating and managing API keys (Ephemeral Sessions) +- Making API calls through Routstr +- Using the admin dashboard + +## Prerequisites + +### 💰 Wallet + +Cashu ([cashu.me](https://cashu.me)) or Lightning ([Strike](https://strike.me), Cash App, etc.) + +### 🌐 Provider + +A Routstr node, e.g. `https://api.routstr.com/v1` + +### 🤖 Client + +OpenAI SDK, Claude Code, Cursor, or any OpenAI-compatible tool + +--- + +## How Routstr Works + +Routstr is a **Payment Proxy**. It sits between your code and the AI provider. + +### Traditional API vs Routstr + +| Traditional | Routstr | +|---|---| +| Credit Card Required | Bitcoin / Lightning / eCash | +| Monthly Billing | Pay-per-request (Real-time) | +| KYC / Account | No Account / Private | +| Single Provider | Aggregated Providers | + +### Key Concepts + +#### 1. Cashu eCash + +Digital bearer tokens backed by Bitcoin. They are instant, private, and have no fees for internal transfers. Routstr uses these tokens as the "credits" for API requests. + +#### 2. Ephemeral Sessions (API Keys) + +Instead of a permanent account, you create a **Session**. + +- You fund a session with eCash or Lightning. +- Routstr gives you an `api_key` (`sk-...`) representing that session. +- You use the `api_key` until funds run out or you finish your task. +- You can **refund** the remaining balance back to your wallet at any time. + +#### 3. Millisats (msats) + +Everything is priced in **millisatoshis**. + +- 1 Satoshi (sat) = 1,000 msats. +- This allows for extremely precise pricing (e.g., 0.05 sats per prompt). + +--- + +## Workflow: Zero to Intelligence + +### 1. Fund a Session + +You need an `api_key` with a balance. + +**Easiest: Use the Web UI** +Visit the node's root page (e.g., [api.routstr.com](https://api.routstr.com)) or [chat.routstr.com](https://chat.routstr.com) → Settings to create a key visually with Lightning. + +**Option A: Lightning Invoice (CLI)** +Generate an invoice and pay it with any Lightning wallet. + +```bash +curl -X POST https://api.routstr.com/lightning/invoice \ + -d '{"amount_sats": 1000, "purpose": "create"}' +``` + +*Returns an invoice (`bolt11`) and an ID. Once paid, the status endpoint returns your `api_key`.* + +**Option B: Cashu Token (Best for privacy & devs)** +If you have a Cashu wallet, you can copy a token string (`cashuA...`) and use it directly. + +- **Direct Usage**: Use the token *as* your API key in the `Authorization` header. +- **Import**: Or exchange it for a standard `sk-...` key: + +```bash +curl "https://api.routstr.com/v1/balance/create?initial_balance_token=cashuA..." +``` + +*Returns your `api_key` immediately.* + +### 2. Configure Your Client + +Use the standard OpenAI SDK, just changing the `base_url` and `api_key`. + +```python +from openai import OpenAI + +client = OpenAI( + base_url="https://api.routstr.com/v1", + api_key="sk-7f8e9d..." # The key from Step 1 +) +``` + +### 3. Make Requests + +```python +response = client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Explain quantum computing."}] +) +``` + +### 4. Withdraw Change + +When you are done, get your change back as a Cashu token. + +```bash +curl -X POST https://api.routstr.com/v1/balance/refund \ + -H "Authorization: Bearer sk-7f8e9d..." +``` + +*Returns a `token` that you can paste back into Nutstash or Minibits to reclaim your funds.* + +--- + +## Supported Features + +- **Responses**: `/v1/responses` (OpenAI Responses API) +- **Chat Completions**: `/v1/chat/completions` (Streaming supported) +- **Embeddings**: `/v1/embeddings` +- **Models**: `/v1/models` (List available models and prices) + +## Next Steps + +- **[Payment Flow](payments.md)**: Detailed breakdown of the funding lifecycle. +- **[Models & Pricing](../provider/pricing.md)**: How costs are calculated. +- **[Admin Dashboard](../provider/dashboard.md)**: Managing your node if you are the operator. diff --git a/docs/client/payments.md b/docs/client/payments.md new file mode 100644 index 0000000..f8d3ffd --- /dev/null +++ b/docs/client/payments.md @@ -0,0 +1,97 @@ +# Payment Flow + +Routstr uses a **Pre-paid, Ephemeral** payment model. Pay first, use the funds, withdraw the rest. No accounts, no credit cards, no trails. + +``` +💰 Deposit → 🤖 Use AI → 💸 Withdraw Change +``` + +## 1. Creating a Balance (Deposit) + +To start making requests, you must create a "Balance" (represented by an API Key). + +### Method A: Lightning Network (Bolt11) + +**Ideal for**: Users connecting from a standard Lightning wallet (Strike, Cash App, WoS). + +1. **Request Invoice**: + `POST /lightning/invoice` with `{"amount_sats": 5000, "purpose": "create"}`. +2. **Pay Invoice**: User scans and pays the QR code/bolt11 string. +3. **Receive Key**: Routstr detects the payment and issues a new API Key (`sk-...`) pre-loaded with 5,000 sats (5,000,000 msats). + +### Method B: Cashu Token Import + +**Ideal for**: Private, instant access or automated agents. + +1. **Generate Token**: User creates a token in their local wallet (e.g., 1000 sats). +2. **Import**: `GET /v1/balance/create?initial_balance_token=cashuA...` +3. **Receive Key**: Routstr claims the token and issues an API Key (`sk-...`) with that balance. + +--- + +## 2. Consuming Funds (Inference) + +Every time you make a request to `/v1/chat/completions` (or others), the cost is deducted from your balance **in real-time**. + +### Cost Calculation + +`Cost = (Input_Tokens * Price_Input) + (Output_Tokens * Price_Output) + Request_Fee` + +- Prices are defined per model (see `/v1/models`). +- If you stream the response, the balance is deducted incrementally or finalized at the end of the stream. +- If your balance hits 0 mid-stream, the connection is closed. + +### Headers + +Routstr checks the `Authorization: Bearer sk-...` header to identify which balance to charge. + +--- + +## 3. Topping Up + +If your balance runs low, you don't need a new key. You can top up the existing one. + +### Via Lightning + +`POST /lightning/invoice` with `{"amount_sats": 1000, "purpose": "topup", "api_key": "sk-..."}`. +*Once paid, the funds are added to your existing key.* + +### Via Cashu + +`POST /v1/balance/topup` with `{"cashu_token": "..."}` and `Authorization: Bearer sk-...`. + +--- + +## 4. Refund (Withdrawal) + +Don't leave large balances sitting on a node—it's a hot wallet. When you're done, get your sats back. + +### Endpoint + +`POST /v1/balance/refund` + +**Headers**: +`Authorization: Bearer sk-...` + +**Response**: + +```json +{ + "token": "cashuAeyJ0b2tlbiI6W3sibWludCI6...", + "msats": "450000" +} +``` + +You can verify the refund was successful by checking that the API Key is now invalid or has 0 balance. Copy the `token` string and paste it into your Cashu wallet to claim the Bitcoin. + +--- + +## Summary + +| Step | Action | Result | +|------|--------|--------| +| 💰 **Deposit** | Pay Lightning invoice or import Cashu | Get `sk-...` key | +| 🤖 **Use** | Make API requests | Balance decreases | +| 💸 **Refund** | Call `/v1/balance/refund` | Get Cashu token back | + +That's it. No monthly bills, no surprise charges, no data harvesting. diff --git a/docs/contributing/architecture.md b/docs/contributing/architecture.md index 45ae0cb..8136bcd 100644 --- a/docs/contributing/architecture.md +++ b/docs/contributing/architecture.md @@ -4,7 +4,7 @@ This document describes the high-level architecture of Routstr Core, helping con ## System Overview -Routstr Core is a FastAPI-based reverse proxy that adds Bitcoin micropayments to OpenAI-compatible APIs. +Routstr Core is a FastAPI-based reverse proxy that adds Bitcoin micropayments to OpenAI-compatible APIs and can optionally announce providers via Nostr. ```mermaid graph TB @@ -20,7 +20,7 @@ graph TB Auth[Auth Module] Payment[Payment Module] Proxy[Proxy Module] - DB[(SQLite DB)] + DB[(SQLModel DB)] API --> Auth Auth --> Payment @@ -40,57 +40,87 @@ graph TB The main application is initialized in `routstr/core/main.py`: -- **Lifespan Management**: Handles startup/shutdown tasks -- **Middleware**: CORS, logging, error handling -- **Routers**: Modular endpoint organization -- **Background Tasks**: Price updates, automatic payouts +- **Lifespan Management**: Runs migrations, initializes DB, refreshes pricing/models, starts background tasks +- **Middleware**: CORS and request logging +- **Routers**: Admin, pricing/models, balance/wallet, providers discovery, proxy +- **Background Tasks**: Price refresh, model map refresh, payouts, node announcements, provider discovery refresh ### Authentication System Located in `routstr/auth.py`, handles: -- **API Key Validation**: Hashed key storage and lookup -- **Balance Checking**: Ensures sufficient funds -- **Rate Limiting**: Optional request throttling -- **Token Redemption**: Converts eCash to balance +- **API Key Validation**: SHA-256 hashed key lookup and persistence +- **Balance Checking**: Ensures sufficient funds before requests +- **Token Redemption**: Converts Cashu tokens to balance ### Payment Processing The `routstr/payment/` module manages: - **Cost Calculation**: Token-based or fixed pricing -- **Model Pricing**: Dynamic pricing from models.json -- **Currency Conversion**: BTC/USD rate management -- **Fee Application**: Exchange and provider fees +- **Model Pricing**: Derived from upstream providers and DB overrides +- **Currency Conversion**: BTC/USD price refresh and conversion +- **Fee Application**: Provider fee applied to upstream model pricing ### Request Proxying `routstr/proxy.py` handles: -- **Request Forwarding**: Preserves headers and body -- **Response Streaming**: Efficient memory usage -- **Usage Tracking**: Counts tokens and costs -- **Error Handling**: Graceful upstream failures +- **Request Forwarding**: Forwards requests to selected upstream providers +- **Response Streaming**: Streaming and non-streaming paths +- **Usage Tracking**: Adjusts costs after upstream responses +- **Error Handling**: Maps upstream errors to consistent responses ### Database Layer Using SQLModel in `routstr/core/db.py`: ```python -# Core models -APIKey: - - id: Primary key - - key_hash: Hashed API key +# Core tables +ApiKey: + - hashed_key: Primary key (SHA-256 of key or Cashu token) - balance: Current balance (msats) - - created_at: Timestamp - - metadata: JSON field + - reserved_balance: Reserved balance (msats) + - refund_address: Optional LNURL for refunds + - key_expiry_time: Optional refund expiry timestamp + - total_spent: Total spent (msats) + - total_requests: Request count + - refund_mint_url: Mint URL for refunds + - refund_currency: Refund currency -Transaction: +UpstreamProviderRow: - id: Primary key - - api_key_id: Foreign key - - amount: Transaction amount - - type: deposit/usage/withdrawal - - timestamp: When occurred + - provider_type: openai/anthropic/azure/openrouter/etc. + - base_url: Provider API base URL + - api_key: Provider API key + - api_version: Optional API version + - enabled: Provider enabled flag + - provider_fee: Provider fee multiplier + +ModelRow: + - id: Model ID + - upstream_provider_id: Provider foreign key + - name: Model name + - architecture: JSON + - pricing: JSON + - sats_pricing: JSON + - per_request_limits: JSON + - top_provider: JSON + - canonical_slug: Canonical model slug + - alias_ids: Model aliases + - enabled: Model enabled flag + +LightningInvoice: + - id: Primary key + - bolt11: Invoice + - amount_sats: Amount in sats + - payment_hash: Payment hash + - status: pending/paid/expired/cancelled + - api_key_hash: Optional associated API key + - purpose: create/topup + - created_at: Unix timestamp + - expires_at: Unix timestamp + - paid_at: Unix timestamp ``` ## Request Flow @@ -107,10 +137,10 @@ sequenceDiagram C->>R: API Request + Key R->>D: Validate Key D-->>R: Key Info + Balance - R->>R: Check Balance + R->>R: Reserve Max Cost R->>P: Forward Request P-->>R: AI Response - R->>D: Deduct Cost + R->>D: Finalize Cost (adjust by usage) R-->>C: Return Response ``` @@ -124,36 +154,20 @@ sequenceDiagram participant M as Cashu Mint participant D as Database - C->>R: Create Key Request + Token - R->>W: Validate Token + C->>R: Request + Cashu Token + R->>W: Redeem Token W->>M: Verify with Mint M-->>W: Token Valid W-->>R: Token Amount - R->>D: Create Key + Balance - R-->>C: Return API Key + R->>D: Create/Update Key + Balance + R-->>C: Continue Request ``` ## Key Design Decisions ### 1. Async Architecture -Everything is async for maximum performance: - -```python -async def handle_request(request: Request) -> Response: - # Non-blocking database queries - api_key = await get_api_key(request.headers["Authorization"]) - - # Concurrent operations - balance_check, rate_limit = await asyncio.gather( - check_balance(api_key), - check_rate_limit(api_key) - ) - - # Stream response without blocking - async for chunk in proxy_request(request): - yield chunk -``` +The system is async end-to-end, with background tasks for pricing refresh, provider discovery, model map refresh, and payouts. ### 2. Modular Design @@ -166,82 +180,46 @@ Components are loosely coupled: ### 3. Error Handling -Graceful degradation and clear error messages: - -```python -class RoustrError(Exception): - """Base exception with structured error response""" - status_code: int = 500 - error_type: str = "internal_error" - -class InsufficientBalanceError(RoustrError): - status_code = 402 - error_type = "insufficient_balance" -``` +Exceptions are handled by FastAPI exception handlers to return consistent JSON responses with a request ID. ### 4. Database Migrations -Using Alembic for schema management: - -- Auto-migrations on startup -- Version control for schema changes -- Rollback capability -- Zero-downtime updates +Alembic migrations are run on startup, and tables are created for any models not tracked by migrations. ## Security Architecture ### API Key Security - **Storage**: SHA-256 hashed keys -- **Generation**: Cryptographically secure random -- **Validation**: Constant-time comparison -- **Rotation**: Support for key expiry +- **Generation**: Cryptographically secure random (when creating new keys) +- **Validation**: Hash lookup in DB +- **Expiry**: Optional refund flow via `key_expiry_time` and `refund_address` ### Payment Security -- **Token Validation**: Cryptographic verification -- **Double-Spend Prevention**: Mint verification -- **Balance Protection**: Atomic transactions -- **Audit Trail**: All transactions logged +- **Token Validation**: Cashu token redemption via mint +- **Balance Protection**: Atomic updates and reserved balance tracking +- **Audit Trail**: Structured logging of payments and adjustments ### Network Security -- **HTTPS**: Enforced in production - **CORS**: Configurable origins -- **Rate Limiting**: Per-key limits - **Input Validation**: Pydantic models ## Performance Considerations ### Caching Strategy -```python -# Model pricing cache -@lru_cache(maxsize=100) -def get_model_price(model_id: str) -> ModelPrice: - return MODELS.get(model_id) - -# Balance cache with TTL -balance_cache = TTLCache(maxsize=1000, ttl=60) -``` +Model and provider selections are cached in process memory and refreshed on a schedule. ### Database Optimization -- **Connection Pooling**: Reuse connections -- **Indexed Queries**: Key lookups are O(1) -- **Batch Operations**: Group updates - **Async I/O**: Non-blocking queries +- **Atomic Updates**: Balance reservation and finalization updates ### Streaming Responses -Efficient memory usage for large responses: - -```python -async def stream_response(upstream_response): - async for chunk in upstream_response.aiter_bytes(): - # Process chunk without loading full response - yield process_chunk(chunk) -``` +Streaming responses are forwarded from upstream providers with usage tracking hooks. ## Extension Points @@ -273,8 +251,6 @@ async def stream_response(upstream_response): - Mock external dependencies - Test business logic in isolation -- Fast execution (< 1 second per test) -- High coverage target (> 80%) ### Integration Tests @@ -285,10 +261,7 @@ async def stream_response(upstream_response): ### Performance Tests -- Load testing with locust -- Memory profiling -- Database query optimization -- Response time benchmarks +- Response time benchmarks (as needed) ## Monitoring and Observability @@ -308,41 +281,17 @@ logger.info("api_request", extra={ ### Metrics Collection -Key metrics tracked: - -- Request rate by endpoint -- Token usage by model -- Balance changes -- Error rates -- Response times +Structured logs are emitted for requests, pricing, and payment events. ### Health Checks -```python -@app.get("/health") -async def health_check(): - return { - "status": "healthy", - "version": __version__, - "database": await check_db(), - "upstream": await check_upstream(), - "mints": await check_mints() - } -``` +Use `/v1/info` for basic service metadata and configuration visibility. ## Deployment Architecture ### Container Structure -```dockerfile -# Multi-stage build -FROM python:3.11-slim AS builder -# Install dependencies - -FROM python:3.11-slim -# Copy only runtime needs -# Run as non-root user -``` +See `core/Dockerfile` for the current container build configuration. ### Environment Configuration @@ -352,10 +301,9 @@ FROM python:3.11-slim ### Scaling Considerations -- **Horizontal**: Multiple instances behind load balancer +- **Horizontal**: Multiple instances behind a load balancer - **Vertical**: Async handles high concurrency -- **Database**: Consider PostgreSQL for scale -- **Caching**: Redis for distributed cache +- **Database**: Configure `DATABASE_URL` for external databases ## Future Architecture diff --git a/docs/contributing/code-structure.md b/docs/contributing/code-structure.md index 3ecd4f5..4043216 100644 --- a/docs/contributing/code-structure.md +++ b/docs/contributing/code-structure.md @@ -7,10 +7,13 @@ This guide provides a detailed overview of Routstr Core's codebase organization ``` routstr-core/ ├── routstr/ # Main application package -│ ├── __init__.py # Package initialization, loads .env -│ ├── auth.py # Authentication and authorization +│ ├── __init__.py # Package initialization, exports FastAPI app +│ ├── algorithm.py # Model selection/mapping logic +│ ├── auth.py # Bearer/Cashu auth and payment handling │ ├── balance.py # Balance management endpoints │ ├── discovery.py # Nostr relay discovery +│ ├── lightning.py # Lightning invoice topups +│ ├── nip91.py # Node announcement logic │ ├── proxy.py # Request proxying logic │ ├── wallet.py # Cashu wallet operations │ │ @@ -18,19 +21,23 @@ routstr-core/ │ │ ├── __init__.py │ │ ├── admin.py # Admin dashboard and API │ │ ├── db.py # Database models and connection -│ │ ├── exceptions.py # Custom exception classes +│ │ ├── exceptions.py # Exception handlers │ │ ├── logging.py # Structured logging setup │ │ ├── main.py # FastAPI app initialization │ │ └── middleware.py # HTTP middleware components │ │ -│ └── payment/ # Payment processing -│ ├── __init__.py -│ ├── cost_calculation.py # Usage cost calculation -│ ├── helpers.py # Payment utilities -│ ├── lnurl.py # Lightning URL support -│ ├── models.py # Model pricing management -│ ├── price.py # BTC/USD price handling -│ └── x_cashu.py # Cashu header protocol +│ ├── payment/ # Payment processing +│ │ ├── __init__.py +│ │ ├── cost_calculation.py # Usage cost calculation +│ │ ├── helpers.py # Payment utilities +│ │ ├── lnurl.py # Lightning URL support +│ │ ├── models.py # Model pricing management +│ │ └── price.py # BTC/USD price handling +│ │ +│ └── upstream/ # Upstream provider integrations +│ ├── base.py # Base provider logic +│ ├── helpers.py # Provider init and model refresh +│ └── ... # Provider implementations │ ├── tests/ # Test suite │ ├── __init__.py @@ -45,7 +52,12 @@ routstr-core/ │ └── versions/ # Migration files │ ├── scripts/ # Utility scripts -│ └── models_meta.py # Fetch model pricing +│ ├── models_meta.py # Fetch model pricing +│ └── ... # Build/update helpers +│ +├── examples/ # Example clients +├── testing-clients/ # HTML test clients +├── ui/ # Next.js admin UI │ ├── docs/ # Documentation ├── logs/ # Application logs (git ignored) @@ -71,30 +83,27 @@ routstr-core/ #### `routstr/__init__.py` ```python -# Loads environment variables -import dotenv -dotenv.load_dotenv() - -# Exports FastAPI app from .core.main import app as fastapi_app + +__all__ = ["fastapi_app"] ``` #### `routstr/core/main.py` ```python # FastAPI application setup -app = FastAPI( - title="Routstr Node", - lifespan=lifespan, # Manages startup/shutdown -) +app = FastAPI(version=__version__, lifespan=lifespan) # Middleware registration app.add_middleware(CORSMiddleware, ...) app.add_middleware(LoggingMiddleware) # Router inclusion +app.include_router(models_router) app.include_router(admin_router) app.include_router(balance_router) +app.include_router(deprecated_wallet_router) +app.include_router(providers_router) app.include_router(proxy_router) ``` @@ -102,29 +111,24 @@ app.include_router(proxy_router) #### `routstr/auth.py` -Handles API key validation and authorization: +Handles bearer key validation and payment lifecycle (bearer or Cashu token): ```python -class APIKeyAuth: - """FastAPI dependency for API key authentication""" - - async def __call__(self, request: Request) -> APIKey: - # Extract and validate API key - # Check balance - # Return authenticated key object - -# Usage in routes: -@router.get("/protected") -async def protected_route(api_key: APIKey = Depends(APIKeyAuth())): - pass +async def validate_bearer_key( + bearer_key: str, + session: AsyncSession, + refund_address: Optional[str] = None, + key_expiry_time: Optional[int] = None, +) -> ApiKey: + """Validate bearer API key or redeem Cashu token into a balance.""" ``` Key functions: -- `create_api_key()` - Generate new API keys -- `validate_api_key()` - Verify and retrieve key -- `check_balance()` - Ensure sufficient funds -- `update_last_used()` - Track usage +- `validate_bearer_key()` - Validate API key or Cashu token +- `pay_for_request()` - Reserve max cost before upstream call +- `adjust_payment_for_tokens()` - Adjust final cost after response +- `revert_pay_for_request()` - Refund on upstream failure ### Payment Processing @@ -133,56 +137,30 @@ Key functions: Calculates request costs: ```python -def calculate_request_cost( - model: str, - prompt_tokens: int, - completion_tokens: int, - **kwargs -) -> CostData: - """Calculate cost in millisatoshis""" - # Model-based or fixed pricing - # Token counting - # Fee application - # Currency conversion +async def calculate_cost( + response_data: dict, max_cost: int, session: AsyncSession +) -> CostData | MaxCostData | CostDataError: + """Calculate cost in millisatoshis from response usage or model pricing.""" ``` #### `routstr/payment/models.py` -Manages model pricing data: +Manages model pricing, database overrides, and pricing refresh: ```python -class ModelPrice: +class Model(BaseModel): id: str name: str - pricing: dict[str, float] # USD prices - context_length: int - -# Global model registry -MODELS: dict[str, ModelPrice] = load_models() + pricing: Pricing + sats_pricing: Pricing | None = None -# Dynamic price updates async def update_sats_pricing(): - """Background task to update BTC prices""" + """Periodic task to update sats pricing for providers and overrides.""" ``` -#### `routstr/payment/x_cashu.py` +#### `routstr/proxy.py` + `routstr/upstream/*` -Implements Cashu payment protocol: - -```python -class XCashuHandler: - """Handle x-cashu header payments""" - - async def process_request_payment( - self, - token: str, - estimated_cost: int - ) -> PaymentResult: - # Validate token - # Check minimum amount - # Process payment - # Generate change -``` +The `x-cashu` header is handled by the proxy route and delegated to upstream providers. ### Request Proxying @@ -191,17 +169,11 @@ class XCashuHandler: Core proxy functionality: ```python -@router.api_route("/{path:path}", methods=ALL_METHODS) -async def proxy_request( - request: Request, - path: str, - api_key: APIKey = Depends(APIKeyAuth()) -) -> Response: - """Forward requests to upstream provider""" - # Build upstream request - # Stream response - # Track usage - # Deduct costs +@proxy_router.api_route("/{path:path}", methods=["GET", "POST"], response_model=None) +async def proxy( + request: Request, path: str, session: AsyncSession = Depends(get_session) +) -> Response | StreamingResponse: + """Forward requests to upstream provider and charge usage.""" ``` Key features: @@ -215,27 +187,29 @@ Key features: #### `routstr/core/db.py` -SQLModel definitions: +SQLModel definitions (selected): ```python -class APIKey(SQLModel, table=True): - id: int | None = Field(primary_key=True) - key_hash: str = Field(index=True, unique=True) - balance: int # millisatoshis - total_deposited: int = 0 +class ApiKey(SQLModel, table=True): + hashed_key: str = Field(primary_key=True) + balance: int + reserved_balance: int = 0 + refund_address: str | None = None + key_expiry_time: int | None = None total_spent: int = 0 - created_at: datetime - expires_at: datetime | None = None - metadata: dict = Field(default_factory=dict, sa_column=Column(JSON)) + total_requests: int = 0 -class Transaction(SQLModel, table=True): - id: int | None = Field(primary_key=True) - api_key_id: int = Field(foreign_key="apikey.id") - amount: int # can be negative - balance_after: int - type: TransactionType - description: str - timestamp: datetime +class LightningInvoice(SQLModel, table=True): + id: str = Field(primary_key=True) + bolt11: str + amount_sats: int + status: str + +class UpstreamProviderRow(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + provider_type: str + base_url: str + api_key: str ``` ### Admin Interface @@ -245,18 +219,17 @@ class Transaction(SQLModel, table=True): Web dashboard and admin API: ```python -@admin_router.get("/admin/") +@admin_router.get("/admin") async def admin_dashboard(request: Request): """Render admin HTML interface""" # Authentication check # Load statistics # Render template -@admin_router.post("/admin/api/withdraw") +@admin_router.post("/admin/withdraw") async def withdraw_balance( - api_key: str, - amount: int | None = None -) -> WithdrawalResponse: + request: Request, withdraw_request: WithdrawRequest +) -> dict[str, str]: """Generate eCash token for withdrawal""" ``` @@ -271,25 +244,14 @@ Features: #### `routstr/wallet.py` -Cashu wallet operations: +Cashu wallet operations (function-based): ```python -class WalletManager: - """Manage Cashu wallet instances""" - - async def redeem_token( - self, - token: str, - mint_url: str | None = None - ) -> int: - """Redeem eCash token and return value""" - - async def create_token( - self, - amount: int, - mint_url: str - ) -> str: - """Create eCash token for withdrawal""" +async def recieve_token(token: str) -> tuple[int, str, str]: + """Redeem eCash token and return amount/unit/mint.""" + +async def send_token(amount: int, unit: str, mint_url: str | None = None) -> str: + """Create eCash token for withdrawal.""" ``` ### Utility Modules @@ -301,12 +263,9 @@ Structured logging configuration: ```python def setup_logging(): """Configure JSON structured logging""" - # Set log level - # Configure formatters - # Add handlers - -class RequestIdMiddleware: - """Add request ID to all logs""" + +class RequestIdFilter(logging.Filter): + """Attach request ID to log records.""" ``` #### `routstr/core/middleware.py` @@ -316,27 +275,18 @@ HTTP middleware components: ```python class LoggingMiddleware: """Log all HTTP requests/responses""" - -class ErrorHandlingMiddleware: - """Consistent error responses""" ``` #### `routstr/core/exceptions.py` -Custom exception hierarchy: +Exception handlers: ```python -class RoustrError(Exception): - """Base exception with error details""" - status_code: int - error_type: str - detail: str +async def http_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """HTTP exception handler with request ID""" -class PaymentError(RoustrError): - """Payment-related errors""" - -class UpstreamError(RoustrError): - """Upstream API errors""" +async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """Fallback exception handler with request ID""" ``` ## Configuration Files @@ -348,7 +298,7 @@ Project metadata and dependencies: ```toml [project] name = "routstr" -version = "0.2.0" +version = "0.2.2" dependencies = [ "fastapi[standard]>=0.115", "sqlmodel>=0.0.24", @@ -409,13 +359,13 @@ Using FastAPI's DI system: ```python # Define dependency -async def get_db() -> AsyncSession: - async with async_session() as session: +async def get_session() -> AsyncGenerator[AsyncSession, None]: + async with AsyncSession(engine, expire_on_commit=False) as session: yield session # Use in routes @router.get("/items") -async def get_items(db: AsyncSession = Depends(get_db)): +async def get_items(db: AsyncSession = Depends(get_session)): result = await db.execute(select(Item)) return result.scalars().all() ``` diff --git a/docs/contributing/setup.md b/docs/contributing/setup.md index 35f72f1..e1576e0 100644 --- a/docs/contributing/setup.md +++ b/docs/contributing/setup.md @@ -22,22 +22,7 @@ git clone https://github.com/YOUR_USERNAME/routstr-core.git cd routstr-core ``` -### 2. Install uv - -We use [uv](https://github.com/astral-sh/uv) for fast, reliable Python package management: - -```bash -# Using the installer script -curl -LsSf https://astral.sh/uv/install.sh | sh - -# Or with pip -pip install uv - -# Or with Homebrew (macOS) -brew install uv -``` - -### 3. Set Up Environment +### 2. Set Up Environment Run the setup command: @@ -47,13 +32,13 @@ make setup This will: -- ✅ Install uv if not present +- ✅ Install [uv](https://github.com/astral-sh/uv) if not present - ✅ Create a virtual environment - ✅ Install all dependencies - ✅ Install dev tools (mypy, ruff, pytest) - ✅ Install project in editable mode -### 4. Configure Environment +### 3. Configure Environment Create your environment file: @@ -71,7 +56,7 @@ ADMIN_PASSWORD=development-password DATABASE_URL=sqlite+aiosqlite:///dev.db ``` -### 5. Verify Installation +### 4. Verify Installation Run these commands to verify your setup: @@ -168,42 +153,92 @@ Understanding the codebase: ``` routstr-core/ ├── routstr/ # Main package -│ ├── __init__.py # Package initialization +│ ├── __init__.py +│ ├── algorithm.py # Provider selection algorithms │ ├── auth.py # Authentication logic -│ ├── balance.py # Balance management +│ ├── balance.py # Balance management API │ ├── discovery.py # Nostr discovery +│ ├── lightning.py # Lightning invoice handling +│ ├── nip91.py # Node announcement implementation │ ├── proxy.py # Request proxying │ ├── wallet.py # Cashu wallet integration │ │ │ ├── core/ # Core modules -│ │ ├── admin.py # Admin dashboard -│ │ ├── db.py # Database models +│ │ ├── admin.py # Admin dashboard API +│ │ ├── db.py # Database models (SQLModel) │ │ ├── exceptions.py # Custom exceptions +│ │ ├── log_manager.py # Log management │ │ ├── logging.py # Logging setup -│ │ ├── main.py # FastAPI app -│ │ └── middleware.py # HTTP middleware +│ │ ├── main.py # FastAPI app entry +│ │ ├── middleware.py # HTTP middleware +│ │ └── settings.py # Configuration │ │ -│ └── payment/ # Payment processing -│ ├── cost_calculation.py # Cost logic -│ ├── helpers.py # Utilities -│ ├── lnurl.py # Lightning URLs -│ ├── models.py # Model pricing -│ ├── price.py # BTC pricing -│ └── x_cashu.py # Cashu headers +│ ├── payment/ # Payment processing +│ │ ├── cost_calculation.py +│ │ ├── helpers.py +│ │ ├── lnurl.py # LNURL support +│ │ ├── models.py # Model pricing +│ │ └── price.py # BTC/USD rates +│ │ +│ └── upstream/ # Upstream providers +│ ├── base.py # Base provider class +│ ├── helpers.py # Shared utilities +│ ├── openai.py # OpenAI +│ ├── anthropic.py # Anthropic +│ ├── gemini.py # Google Gemini +│ ├── openrouter.py # OpenRouter +│ └── ... # More providers +│ +├── ui/ # Admin dashboard (Next.js) +│ ├── app/ # Next.js app router +│ │ ├── page.tsx # Landing page +│ │ ├── balances/ # Balance management +│ │ ├── logs/ # Request logs viewer +│ │ ├── model/ # Model configuration +│ │ ├── providers/ # Upstream providers +│ │ ├── settings/ # Node settings +│ │ └── transactions/ # Transaction history +│ ├── components/ # React components +│ │ ├── ui/ # shadcn/ui primitives +│ │ ├── landing/ # Landing page components +│ │ └── settings/ # Settings components +│ └── lib/ # Utilities & API client +│ ├── api/ # Backend API client +│ ├── auth/ # Auth context +│ └── hooks/ # React hooks │ ├── tests/ # Test suite -│ ├── unit/ # Unit tests -│ └── integration/ # Integration tests +├── migrations/ # Alembic migrations +├── scripts/ # Utility scripts +├── docs/ # Documentation +├── examples/ # Usage examples │ -├── migrations/ # Database migrations -├── scripts/ # Utility scripts -├── docs/ # Documentation -│ -├── Makefile # Dev commands -├── pyproject.toml # Project config -└── compose.yml # Docker setup +├── Makefile # Dev commands +├── pyproject.toml # Project config +└── compose.yml # Docker setup ``` +### Admin Dashboard (UI) + +The admin dashboard is a Next.js app using: + +- **Next.js 14** with App Router +- **shadcn/ui** for components +- **Tailwind CSS** for styling +- **pnpm** for package management + +```bash +# Development +cd ui +pnpm install +pnpm dev # http://localhost:3000 + +# Build for production +pnpm build +``` + +The UI is served by the FastAPI backend at `/admin/` when built. Use `make build-ui` to build and copy to the backend. + ## Common Tasks ### Adding a New Endpoint @@ -383,7 +418,7 @@ rm -rf test_*.db Now that you're set up: 1. Read the [Architecture Overview](architecture.md) -3. Check [open issues](https://github.com/routstr/routstr-core/issues) -4. Start with a small contribution +2. Check [open issues](https://github.com/routstr/routstr-core/issues) +3. Start with a small contribution Happy coding! 🚀 diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md index d8f7512..a80cccb 100644 --- a/docs/contributing/testing.md +++ b/docs/contributing/testing.md @@ -6,576 +6,228 @@ This guide covers testing practices, patterns, and tools used in Routstr Core de We follow these principles: -- **Test Behavior, Not Implementation** - Tests should survive refactoring -- **Fast Feedback** - Unit tests run in milliseconds -- **Reliable Tests** - No flaky tests allowed -- **Clear Failures** - Tests should clearly indicate what broke +- Test behavior, not implementation +- Fast feedback +- Reliable tests +- Clear failures ## Test Structure ``` tests/ -├── __init__.py -├── conftest.py # Shared fixtures and configuration -├── unit/ # Fast, isolated unit tests -│ ├── test_auth.py -│ ├── test_balance.py -│ ├── test_cost_calculation.py -│ ├── test_models.py -│ └── test_wallet.py -└── integration/ # Component integration tests - ├── test_api_endpoints.py - ├── test_payment_flow.py - ├── test_proxy_streaming.py - └── test_real_mint.py +├── integration/ +│ ├── conftest.py +│ ├── utils.py +│ ├── test_wallet_topup.py +│ ├── test_wallet_refund.py +│ ├── test_wallet_information.py +│ ├── test_proxy_get_endpoints.py +│ ├── test_proxy_post_endpoints.py +│ └── ... more integration tests +├── unit/ +│ ├── test_algorithm.py +│ ├── test_fee_consistency.py +│ ├── test_image_tokens.py +│ ├── test_logging_securityfilter.py +│ ├── test_payment_helpers.py +│ ├── test_settings.py +│ ├── test_wallet.py +│ └── ... more unit tests +└── run_integration.py ``` ## Running Tests -### Quick Test Commands +### Make Targets + +```bash +# Run all tests (unit + integration with mocks) +make test + +# Unit tests only +make test-unit + +# Integration tests with mocks (fast) +make test-integration + +# Integration tests with Docker services +make test-integration-docker + +# Fast tests only (skip slow and Docker tests) +make test-fast + +# Performance tests +make test-performance + +# Coverage +make test-coverage +``` + +### Direct pytest Commands ```bash # Run all tests -make test +pytest -# Run unit tests only (fast) -make test-unit +# Run a specific test file +pytest tests/unit/test_wallet.py -v -# Run integration tests -make test-integration +# Run a specific test +pytest tests/unit/test_wallet.py::test_get_balance -v -# Run with coverage -make test-coverage - -# Run specific test file -uv run pytest tests/unit/test_auth.py -v - -# Run specific test -uv run pytest tests/unit/test_auth.py::test_create_api_key -v - -# Run tests matching pattern -uv run pytest -k "balance" -v +# Run tests matching a pattern +pytest -k "wallet" -v ``` -### Test Markers +## Test Modes (Integration) -Use markers to categorize tests: +Integration tests support two execution modes: -```python -@pytest.mark.slow -async def test_heavy_computation(): - pass +- Mock mode (default): uses in-memory mocks, no Docker required +- Docker mode: uses real Docker services (Cashu mint, mock OpenAI, Nostr relay) -@pytest.mark.requires_docker -async def test_real_services(): - pass +Use the runner script for Docker mode: -# Run without slow tests -pytest -m "not slow" +```bash +./tests/run_integration.py +``` + +Or manually: + +```bash +docker-compose -f compose.testing.yml up -d +USE_LOCAL_SERVICES=1 pytest tests/integration/ -v +docker-compose -f compose.testing.yml down -v +``` + +## Test Markers + +Markers are defined in `pyproject.toml`: + +- `integration` +- `unit` +- `slow` +- `requires_docker` +- `requires_real_mint` +- `performance` +- `asyncio` + +Examples: + +```bash +# Skip slow tests +pytest -m "not slow" -v # Run only integration tests -pytest -m "integration" +pytest -m "integration" -v + +# Run performance tests +pytest -m "performance" -v ``` +## Fixtures and Utilities + +### Core Integration Fixtures + +Defined in `tests/integration/conftest.py`: + +- `integration_client` - Async HTTP client for the FastAPI app +- `authenticated_client` - Client with a pre-created API key +- `testmint_wallet` - Test wallet for generating Cashu tokens +- `db_snapshot` - Database state snapshot/diff helper +- `create_api_key` - Helper to create API keys for tests +- `integration_engine`, `integration_session` - Async DB engine/session +- `background_tasks_controller` - Control background tasks in tests +- `mock_upstream_server` - Mock upstream API responses + +### Integration Utilities + +Defined in `tests/integration/utils.py`: + +- `CashuTokenGenerator` +- `ResponseValidator` +- `PerformanceValidator` +- `ConcurrencyTester` +- `DatabaseStateValidator` +- `MockServiceBuilder` +- `TestDataBuilder` + ## Writing Tests ### Unit Test Example ```python -# tests/unit/test_auth.py -import pytest -from routstr.auth import create_api_key, validate_api_key +from routstr.algorithm import calculate_model_cost_score +from routstr.payment.models import Architecture, Model, Pricing -class TestAPIKeyAuth: - """Test API key authentication functionality""" - - async def test_create_api_key(self, test_db): - """Test creating a new API key""" - # Arrange - initial_balance = 10000 - - # Act - api_key = await create_api_key( - balance=initial_balance, - name="Test Key" - ) - - # Assert - assert api_key.key.startswith("sk-") - assert len(api_key.key) == 32 - assert api_key.balance == initial_balance - - async def test_validate_invalid_key(self, test_db): - """Test validation fails for invalid key""" - # Act & Assert - with pytest.raises(InvalidAPIKeyError): - await validate_api_key("invalid_key") + +def test_calculate_model_cost_score_basic() -> None: + model = Model( + id="test-model", + name="Test test-model", + created=1234567890, + description="Test model", + context_length=8192, + architecture=Architecture( + modality="text", + input_modalities=["text"], + output_modalities=["text"], + tokenizer="gpt", + instruct_type=None, + ), + pricing=Pricing( + prompt=0.001, + completion=0.002, + request=0.0, + image=0.0, + web_search=0.0, + internal_reasoning=0.0, + ), + ) + assert calculate_model_cost_score(model) == 0.002 ``` ### Integration Test Example ```python -# tests/integration/test_payment_flow.py import pytest from httpx import AsyncClient -class TestPaymentFlow: - """Test end-to-end payment flows""" - - @pytest.mark.asyncio - async def test_token_redemption_flow( - self, - app_client: AsyncClient, - mock_cashu_wallet - ): - """Test complete token redemption and API usage""" - # Arrange - token = create_test_token(amount=5000) - mock_cashu_wallet.redeem.return_value = 5000 - - # Act - Create API key - response = await app_client.post( - "/v1/wallet/create", - json={"cashu_token": token} - ) - - # Assert - Key created - assert response.status_code == 200 - api_key = response.json()["api_key"] - assert response.json()["balance"] == 5000 - - # Act - Use API key - response = await app_client.post( - "/v1/chat/completions", - headers={"Authorization": f"Bearer {api_key}"}, - json={ - "model": "gpt-3.5-turbo", - "messages": [{"role": "user", "content": "Hi"}] - } - ) - - # Assert - Request successful - assert response.status_code == 200 - assert "choices" in response.json() -``` -## Test Fixtures - -### Common Fixtures - -Located in `tests/conftest.py`: - -```python -@pytest.fixture -async def test_db(): - """Provide a clean test database""" - # Create in-memory SQLite database - engine = create_async_engine("sqlite+aiosqlite:///:memory:") - - async with engine.begin() as conn: - await conn.run_sync(SQLModel.metadata.create_all) - - async_session = sessionmaker(engine, class_=AsyncSession) - - async with async_session() as session: - yield session - - await engine.dispose() - -@pytest.fixture -async def app_client(test_db): - """Provide test client with test database""" - app.dependency_overrides[get_db] = lambda: test_db - - async with AsyncClient(app=app, base_url="http://test") as client: - yield client - - app.dependency_overrides.clear() - -@pytest.fixture -def mock_cashu_wallet(mocker): - """Mock Cashu wallet for testing""" - mock = mocker.patch("routstr.wallet.Wallet") - mock.return_value.redeem.return_value = 1000 - return mock -``` - -### Using Fixtures - -```python -async def test_with_fixtures( - test_db, # Get test database - app_client, # Get test HTTP client - mock_cashu_wallet # Get mocked wallet -): - # Use fixtures in test - pass -``` - -## Mocking Strategies - -### Mocking External Services - -```python -# Mock upstream API -@pytest.fixture -def mock_openai(mocker): - mock_response = mocker.Mock() - mock_response.json.return_value = { - "choices": [{ - "message": {"content": "Hello!"}, - "finish_reason": "stop" - }], - "usage": { - "prompt_tokens": 10, - "completion_tokens": 20, - "total_tokens": 30 - } - } - - mocker.patch( - "httpx.AsyncClient.post", - return_value=mock_response - ) - -# Mock Cashu mint -@pytest.fixture -def mock_mint(mocker): - mint = mocker.patch("routstr.wallet.Mint") - mint.return_value.check_proof_state.return_value = True - return mint -``` - -### Mocking Time - -```python -from freezegun import freeze_time - -@freeze_time("2024-01-01 12:00:00") -async def test_time_dependent(): - # Time is frozen during test - key = await create_api_key(expires_in_days=30) - assert key.expires_at == datetime(2024, 1, 31, 12, 0, 0) -``` - -## Test Patterns - -### Testing Async Code - -```python -# Always mark async tests +@pytest.mark.integration @pytest.mark.asyncio -async def test_async_function(): - result = await async_operation() - assert result == expected - -# Test async context managers -async def test_async_context(): - async with create_resource() as resource: - assert resource.is_active +async def test_wallet_topup( + authenticated_client: AsyncClient, + testmint_wallet: object, + db_snapshot: object, +) -> None: + await db_snapshot.capture() + token = await testmint_wallet.mint_tokens(1000) + response = await authenticated_client.post( + "/v1/wallet/topup", params={"cashu_token": token} + ) + assert response.status_code == 200 + diff = await db_snapshot.diff() + assert len(diff["api_keys"]["modified"]) == 1 ``` -### Testing Exceptions - -```python -# Test specific exception -async def test_raises_specific_error(): - with pytest.raises(InsufficientBalanceError) as exc_info: - await deduct_balance(api_key, amount=999999) - - assert "Insufficient balance" in str(exc_info.value) - assert exc_info.value.status_code == 402 - -# Test exception details -async def test_exception_details(): - with pytest.raises(RoustrError) as exc_info: - await risky_operation() - - error = exc_info.value - assert error.error_type == "validation_error" - assert error.detail == "Invalid input" -``` - -### Testing Streaming Responses - -```python -async def test_streaming_response(): - """Test streaming chat completion""" - chunks = [] - - async with app_client.stream( - "POST", - "/v1/chat/completions", - json={"model": "gpt-3.5-turbo", "stream": True} - ) as response: - async for line in response.aiter_lines(): - if line.startswith("data: "): - chunks.append(json.loads(line[6:])) - - # Verify chunks - assert len(chunks) > 0 - assert chunks[-1] == "[DONE]" -``` - -### Database Testing - -```python -async def test_database_transaction(test_db): - """Test atomic transactions""" - async with test_db.begin(): - # Create test data - api_key = APIKey(key_hash="test", balance=1000) - test_db.add(api_key) - await test_db.flush() - - # Test rollback - try: - async with test_db.begin_nested(): - api_key.balance = -100 # Invalid - await test_db.flush() - raise ValueError("Rollback") - except ValueError: - pass - - # Verify rollback worked - await test_db.refresh(api_key) - assert api_key.balance == 1000 -``` - -## Performance Testing - -### Benchmark Tests - -```python -@pytest.mark.benchmark -def test_performance(benchmark): - """Benchmark critical functions""" - result = benchmark(expensive_function, arg1, arg2) - assert result == expected - -# Run benchmarks -pytest --benchmark-only -``` - -### Load Testing - -```python -# tests/integration/test_load.py -async def test_concurrent_requests(app_client): - """Test handling multiple concurrent requests""" - async def make_request(i): - response = await app_client.get(f"/test/{i}") - return response.status_code - - # Make 100 concurrent requests - tasks = [make_request(i) for i in range(100)] - results = await asyncio.gather(*tasks) - - # All should succeed - assert all(status == 200 for status in results) -``` - -## Test Data - -### Factories - -```python -# tests/factories.py -from datetime import datetime, timedelta - -def create_test_api_key(**kwargs): - """Factory for test API keys""" - defaults = { - "key": f"sk-test-{uuid4().hex[:8]}", - "balance": 10000, - "created_at": datetime.utcnow(), - "expires_at": datetime.utcnow() + timedelta(days=30) - } - defaults.update(kwargs) - return APIKey(**defaults) - -def create_test_token(amount: int = 1000, mint: str = None): - """Factory for test Cashu tokens""" - # Create valid test token structure - return base64.encode(...) -``` - -### Test Constants - -```python -# tests/constants.py -TEST_MODELS = { - "gpt-3.5-turbo": { - "prompt": 0.0015, - "completion": 0.002 - }, - "gpt-4": { - "prompt": 0.03, - "completion": 0.06 - } -} - -TEST_API_KEY = "sk-test-1234567890" -TEST_MINT_URL = "https://testmint.example.com" -``` - -## Coverage - -### Running Coverage +## Debugging Tips ```bash -# Generate coverage report -make test-coverage +# Show print output +pytest -s tests/unit/test_wallet.py -# View HTML report -open htmlcov/index.html - -# Coverage with specific tests -pytest --cov=routstr --cov-report=html tests/unit/ -``` - -### Coverage Configuration - -In `pyproject.toml`: - -```toml -[tool.coverage.run] -source = ["routstr"] -omit = ["tests/*", "*/migrations/*"] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "raise AssertionError", - "raise NotImplementedError", - "if TYPE_CHECKING:" -] -``` - -## Debugging Tests - -### Print Debugging - -```python -# Use -s flag to see print output -pytest -s tests/unit/test_auth.py - -# In test -async def test_debug(): - print(f"Value: {value}") # Will show with -s - assert value == expected -``` - -### Interactive Debugging - -```python # Drop into debugger on failure pytest --pdb - -# Set breakpoint in test -async def test_debug(): - import pdb; pdb.set_trace() - # Execution stops here ``` -### Logging in Tests - -```python -# Enable debug logging in tests -import logging -logging.basicConfig(level=logging.DEBUG) - -# Or use caplog fixture -async def test_logging(caplog): - with caplog.at_level(logging.INFO): - await function_that_logs() - - assert "Expected message" in caplog.text -``` - -## CI/CD Integration - -### GitHub Actions - -Tests run automatically on: - -- Pull requests -- Pushes to main -- Nightly schedules - -See `.github/workflows/test.yml` for configuration. - -### Pre-commit Hooks - -Install pre-commit hooks: - -```bash -pre-commit install - -# Run manually -pre-commit run --all-files -``` - -## Best Practices - -### Do's - -1. ✅ Write tests first (TDD) -2. ✅ Keep tests simple and focused -3. ✅ Use descriptive test names -4. ✅ Test edge cases -5. ✅ Mock external dependencies -6. ✅ Use fixtures for setup -7. ✅ Assert specific values - -### Don'ts - -1. ❌ Don't test implementation details -2. ❌ Don't use production services -3. ❌ Don't rely on test order -4. ❌ Don't ignore flaky tests -5. ❌ Don't skip error cases -6. ❌ Don't use hard-coded waits -7. ❌ Don't commit commented tests - ## Troubleshooting -### Common Issues - -**Async Test Errors** - -```python -# Wrong -def test_async(): # Missing async - await function() - -# Right -async def test_async(): - await function() -``` - -**Database State** - -```python -# Ensure clean state -@pytest.fixture(autouse=True) -async def cleanup(test_db): - yield - # Cleanup after each test - await test_db.execute("DELETE FROM apikey") - await test_db.commit() -``` - -**Mock Not Working** - -```python -# Check import path -mocker.patch("routstr.wallet.Wallet") # Full path -# Not just "Wallet" -``` +- Docker mode failures: check `docker ps` and `docker-compose -f compose.testing.yml logs` +- Connection errors: make sure ports 3338, 3000, 8000, and 8088 are free +- Slow tests: use `pytest -m "not slow"` or `make test-fast` ## Next Steps -- See [Architecture](architecture.md) for system design -- Read [Setup Guide](setup.md) for environment setup +- See [Architecture](architecture.md) +- Read [Setup Guide](setup.md) diff --git a/docs/index.md b/docs/index.md index aeacbcf..9a72b9b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,83 +1,44 @@ # Routstr Core Documentation -Welcome to the official documentation for **Routstr Core** - a FastAPI-based reverse proxy that enables Bitcoin micropayments for OpenAI-compatible APIs using the Cashu eCash protocol. +**Routstr** is a decentralized protocol for permissionless AI inference. It enables an open marketplace where anyone can buy and sell compute using **Bitcoin eCash (Cashu)**. -## What is Routstr Core? +--- -Routstr Core is a payment proxy that sits between API clients and OpenAI-compatible services. It enables: +## 🐣 For Clients (Users & Builders) -- **Pay-per-request billing** using Bitcoin eCash tokens -- **Seamless integration** with existing OpenAI clients -- **Privacy-preserving payments** through the Cashu protocol -- **Flexible pricing models** with per-token or per-request billing -- **Multi-provider support** for various AI model providers +If you want to use AI models in your application without accounts or KYC. -### Key Features +- **[Introduction](client/introduction.md)**: How the ecosystem works. +- **[Payment Flow](client/payments.md)**: Funding sessions, topping up, and refunds. +- **[Integration Guide](client/integration.md)**: Code examples for Python, JS, and cURL. -- 🪙 **Cashu Wallet Integration** - Accept Lightning payments and redeem eCash tokens -- 🔑 **API Key Management** - Secure key storage with balance tracking -- 💰 **Dynamic Pricing** - Model-based pricing with live BTC/USD conversion -- 🎛️ **Admin Dashboard** - Web interface for balance and key management -- 🌐 **Nostr Discovery** - Find providers through decentralized relay network -- 🐋 **Docker Support** - Easy deployment with optional Tor hidden service -- ⚡ **Lightning Fast** - Minimal latency overhead for API requests +## 🦁 For Providers (Node Operators) -## How It Works +If you want to run a node, resell API access, or monetize hardware. -```mermaid -sequenceDiagram - participant Client - participant Routstr as Routstr Proxy - participant DB as Database - participant Upstream as AI Provider - participant Wallet as Cashu Wallet +- **[Quick Start](provider/quickstart.md)**: Deploy a node in 5 minutes. +- **[Deployment](provider/deployment.md)**: Production Docker setup. +- **[Configuration](provider/configuration.md)**: Environment variables and settings. +- **[Dashboard](provider/dashboard.md)**: Managing your node visually. +- **[Pricing Strategy](provider/pricing.md)**: Setting margins and fees. +- **[Discovery](provider/discovery.md)**: Announcing your node on Nostr. +- **[Tor Support](provider/tor.md)**: Running an anonymous hidden service. - Client->>Routstr: API Request + eCash Token - Routstr->>Wallet: Validate & Redeem Token - Wallet-->>Routstr: Token Value (sats) - Routstr->>DB: Store/Update Balance - Routstr->>Upstream: Forward API Request - Upstream-->>Routstr: API Response + Usage Data - Routstr->>DB: Deduct Actual Cost - Routstr-->>Client: API Response -``` +--- -## Quick Links +## 🔌 API Reference -
+- **[Overview](api/overview.md)**: Base URL, headers, and standards. +- **[Endpoints](api/endpoints.md)**: Full list of REST endpoints. +- **[Authentication](api/authentication.md)**: Handling API keys and tokens. +- **[Errors](api/errors.md)**: Status codes and debugging. -- :rocket: **[Quick Start](getting-started/quickstart.md)** +## 🛠️ Contributing - Get up and running with Docker in minutes +- **[Architecture](contributing/architecture.md)**: System design. +- **[Setup](contributing/setup.md)**: Development environment. +- **[Testing](contributing/testing.md)**: Running tests. -- :gear: **[Configuration](getting-started/configuration.md)** +--- - Learn about environment variables and settings - -- :book: **[User Guide](user-guide/introduction.md)** - - Comprehensive guide for using Routstr - -- :hammer: **[Contributing](contributing/setup.md)** - - Help improve Routstr Core - -
- -## Use Cases - -- **AI Application Developers** - Add Bitcoin payments to your AI apps without managing infrastructure -- **API Resellers** - Resell API access with custom pricing and profit margins -- **Privacy-Focused Users** - Access AI models without revealing personal information -- **Micropayment Experiments** - Test new business models with instant, small payments - -## Getting Help - -- 📖 Browse the [User Guide](user-guide/introduction.md) for detailed usage instructions -- 🐛 Report issues on [GitHub](https://github.com/routstr/routstr-core/issues) -- 💬 Join the community discussions -- 🔧 Check the [API Reference](api/overview.md) for technical details - -## License - -Routstr Core is open source software licensed under the GPLv3. See the [LICENSE](https://github.com/routstr/routstr-core/blob/main/LICENSE) file for details. +*Powered by [Cashu](https://cashu.space) and [Nostr](https://nostr.com).* diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..9e0ddbc --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,76 @@ +# Overview + +Routstr is a decentralized protocol for **permissionless, private, and censorship-resistant AI inference**. It creates an open marketplace where anyone can sell llm-tokens and anyone can buy them using privacy-preserving micropayments. + +By combining **Nostr** (for censorship-resistant discovery and communication) and **Cashu** (for private, instant Bitcoin eCash payments), Routstr effectively removes the "middleman" from the AI ecosystem. + +## How it Works + +The network consists of independent **Providers** (Sellers) and **Clients** (Buyers). There is no central server, no login, and no credit card required. + +1. **Discovery (Nostr)**: Providers announce their availability, models (e.g., `gpt-4o`, `deepseek-r1`), and prices on the Nostr network. +2. **Payment (Cashu)**: Clients pay providers directly using Bitcoin eCash (Cashu tokens). These payments are untraceable and settle instantly. +3. **Inference (Proxy)**: The Provider acts as a gateway (or runs local hardware), executing the AI model and returning the result to the Client. + +## Who is this for? + +The documentation is split into two paths depending on your goal: + +### 🐣 I want to BUILD on Routstr (Client) + +You are a developer building an AI agent, a chat app, or a script, and you want access to AI models without API keys, subscriptions, or KYC. + +* **No Accounts**: Just get a wallet. +* **Privacy**: Your requests are mixed with thousands of others; providers can't profile you. +* **Choice**: Switch between hundreds of providers instantly for the best price/performance. + +👉 **[Go to Client Guide](client/introduction.md)** + +### 🦁 I want to RUN a Node (Provider) + +You have API credits (OpenAI, Anthropic, etc.) or GPU capacity and want to earn Bitcoin by selling AI access to the network. + +* **Monetize API Keys**: Connect your OpenAI/Anthropic/OpenRouter accounts and earn sats on every request. +* **Monetize Hardware**: Run local models (via vLLM, Ollama) and sell access. +* **Permissionless**: No approval needed. Start the container, configure via dashboard, start earning. + +!!! note "Coming Soon" + Future versions will support node-to-node routing—run a gateway without needing your own AI provider credentials. + +👉 **[Go to Provider Guide](provider/quickstart.md)** + +--- + +## Architecture + +Routstr is built on a modular stack defined by the [Routstr Improvement Protocols (RIPs)](https://github.com/routstr/rips). + +```mermaid +flowchart LR + subgraph Client + A[App / Agent] + end + + subgraph Provider + B[Routstr Node
Proxy + Auth + Billing] + end + + subgraph Upstream + C[OpenAI / Anthropic
vLLM / Ollama / ...] + end + + A -- "Request +
Cashu Token" --> B + B -- "Forward
Request" --> C + C -- "Response +
Usage" --> B + B -- "Response +
Refund Token" --> A +``` + +## Why Routstr? + +| Feature | Closed AI | Routstr | +| :--- | :--- | :--- | +| **Access** | Account, KYC, Credit Card | Permissionless, Bitcoin-native | +| **Privacy** | Full Logging & Tracking | Blinded Payments, Ephemeral Sessions | +| **Resilience** | Single Point of Failure | Decentralized Network | +| **Pricing** | Fixed, Monopolistic | Dynamic, Market-driven | +| **Global** | Geofenced | Borderless (Tor/I2P supported) | diff --git a/docs/provider/advanced-pricing.md b/docs/provider/advanced-pricing.md new file mode 100644 index 0000000..7bb3bd2 --- /dev/null +++ b/docs/provider/advanced-pricing.md @@ -0,0 +1,114 @@ +# Advanced Pricing + +Advanced pricing strategies for fine-tuned control over your revenue model. + +--- + +## Default Behavior + +By default, Routstr: + +1. **Fetches costs** from your upstream provider +2. **Applies markup** using your fee settings +3. **Converts to sats** using real-time BTC price + +**Formula**: `Price = Upstream Cost × Exchange Fee × Upstream Fee` + +--- + +## Strategy 1: Fixed Per-Request + +Charge a flat fee regardless of model or tokens used. + +**Configure in Dashboard** → **Settings** → **Pricing**: + +- Enable **Fixed Pricing** +- Set **Fixed Cost Per Request** (in sats) + +**Use cases**: + +- Internal tools with predictable usage +- Simple "pay once, get response" APIs +- Subscription-like tiers + +--- + +## Strategy 2: Fixed Per-Token + +Override dynamic pricing with global per-token rates. + +**Configure in Dashboard** → **Settings** → **Pricing**: + +| Setting | Description | +|---------|-------------| +| **Fixed Per 1K Input** | Sats per 1,000 prompt tokens | +| **Fixed Per 1K Output** | Sats per 1,000 completion tokens | + +When set to non-zero values, these override model-specific pricing for all models. + +--- + +## Strategy 3: Per-Model Custom Pricing + +Set specific prices for individual models, overriding both upstream cost and global fees. + +**Configure in Dashboard** → **Models**: + +1. Click on a model (e.g., `gpt-4`) +2. Enter **Prompt Price** and **Completion Price** (USD per 1M tokens) +3. Save + +**Example**: OpenAI charges $30/1M for GPT-4. Set your price to $35/1M to lock in a margin regardless of fee settings. + +--- + +## Minimum Charge + +Prevent dust transactions and spam: + +| Setting | Description | Default | +|---------|-------------|---------| +| **Min Request Cost** | Minimum charge in msats | 1000 (1 sat) | + +If a request's calculated cost falls below this (e.g., very short prompts), the client pays the minimum. + +--- + +## Combining Strategies + +Strategies apply in order of specificity: + +1. **Per-model override** (highest priority) +2. **Fixed per-token rates** +3. **Dynamic pricing with fees** (default) +4. **Fixed per-request** (overrides all above if enabled) + +**Example setup**: + +- Dynamic pricing as default (10% markup) +- GPT-4 locked at $35/1M (premium model) +- Claude Haiku at 5 sats/1K tokens (budget option) +- Minimum 1 sat per request + +--- + +## Pricing for Profit + +### High-Volume Strategy + +Lower margins, more clients: + +- Exchange Fee: 1.002 (0.2%) +- Upstream Fee: 1.05 (5%) + +### Premium Strategy + +Higher margins, fewer clients: + +- Exchange Fee: 1.01 (1%) +- Upstream Fee: 1.25 (25%) + +### Mixed Strategy + +- Cheap models (GPT-3.5, Haiku): Low margin to attract volume +- Premium models (GPT-4, Opus): High margin for profit diff --git a/docs/provider/configuration.md b/docs/provider/configuration.md new file mode 100644 index 0000000..aaa00e8 --- /dev/null +++ b/docs/provider/configuration.md @@ -0,0 +1,122 @@ +# Configuration + +Routstr is configured primarily through the **Admin Dashboard**. All settings persist in the database and take effect immediately—no restarts required. + +For automated deployments, you can optionally pre-configure settings via environment variables. + +--- + +## Admin Dashboard (Primary) + +Access the dashboard at `/admin/` on your node. + +### Upstream Providers + +Connect to your AI provider(s): + +| Setting | Description | +|---------|-------------| +| **Upstream URL** | API endpoint (e.g., `https://api.openai.com/v1`) | +| **API Key** | Your provider's API key | + +### Node Identity + +How your node appears to clients: + +| Setting | Description | +|---------|-------------| +| **Name** | Display name (e.g., "Fast GPT-4 Node") | +| **Description** | Brief description of your service | + +### Pricing + +Control your profit margins: + +| Setting | Description | Default | +|---------|-------------|---------| +| **Fixed Pricing** | Charge flat rate per request vs. per-token | Off | +| **Exchange Fee** | Buffer for BTC volatility | 1.005 (0.5%) | +| **Upstream Fee** | Your profit markup | 1.10 (10%) | + +See [Pricing](pricing.md) for detailed strategies. + +### Cashu Mints + +Which mints to accept payments from: + +| Setting | Description | +|---------|-------------| +| **Mints** | List of trusted Cashu mint URLs | + +### Lightning Withdrawals + +Automatic profit withdrawal: + +| Setting | Description | +|---------|-------------| +| **Lightning Address** | Your LN address for withdrawals | + +### Security + +| Setting | Description | +|---------|-------------| +| **Admin Password** | Password for dashboard access | + +### Nostr Discovery + +Announce your node on the network: + +| Setting | Description | +|---------|-------------| +| **Npub** | Your Nostr public key | +| **Nsec** | Your Nostr private key (for signing) | +| **Relays** | Relays to publish announcements | + +See [Discovery](discovery.md) for details. + +--- + +## Environment Variables (Optional) + +Use environment variables for: + +- **Automated deployments** (CI/CD, infrastructure-as-code) +- **Secrets management** (external secret stores) +- **Initial bootstrap** (set once, manage via dashboard later) + +### All Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `UPSTREAM_BASE_URL` | Upstream API endpoint | — | +| `UPSTREAM_API_KEY` | Upstream API key | — | +| `ADMIN_PASSWORD` | Dashboard password | (none) | +| `DATABASE_URL` | Database connection string | `sqlite+aiosqlite:///keys.db` | +| `NAME` | Node display name | `ARoutstrNode` | +| `DESCRIPTION` | Node description | `A Routstr Node` | +| `NPUB` | Nostr public key (bech32) | — | +| `NSEC` | Nostr private key | — | +| `CASHU_MINTS` | Comma-separated mint URLs | `https://mint.minibits.cash/Bitcoin` | +| `RECEIVE_LN_ADDRESS` | Lightning address for withdrawals | — | +| `TOR_PROXY_URL` | SOCKS5 proxy for Tor | `socks5://127.0.0.1:9050` | +| `CORS_ORIGINS` | Allowed CORS origins | `*` | +| `RELAYS` | Nostr relays (comma-separated) | (default set) | + +### Priority + +Environment variables are read on startup. Dashboard settings override them and persist in the database. Once you change a setting in the dashboard, the env var is ignored for that setting. + +--- + +## Models + +Manage which AI models you offer: + +1. Go to **Models** in the dashboard +2. Models are auto-discovered from your upstream +3. For each model, you can: + - **Enable/Disable** — hide expensive models you don't want to serve + - **Override pricing** — set custom per-token rates + - **Create aliases** — friendly names for models + +See [Pricing](pricing.md) for per-model pricing strategies. diff --git a/docs/provider/dashboard.md b/docs/provider/dashboard.md new file mode 100644 index 0000000..aaa860b --- /dev/null +++ b/docs/provider/dashboard.md @@ -0,0 +1,208 @@ +# Admin Dashboard + +The Admin Dashboard is your command center for managing your Routstr provider node. Configure providers, monitor earnings, manage models, and withdraw profits—all from a web interface. + +**URL**: `http://your-node:8000/admin/` + +--- + +## Overview Tab + +The main dashboard view shows your node's financial status at a glance. + +### Wallet Summary + +| Metric | Description | +|--------|-------------| +| **Total Wallet** | All Bitcoin currently held by your node | +| **User Balances** | Funds belonging to active client sessions | +| **Your Balance** | Your profit: `Total - User Balances` | + +### Mint Status + +Shows connected Cashu mints and their balances. Each mint displays: + +- Connection status +- Balance in sats/msats +- Unit type + + + +--- + +## Sessions Tab + +View and manage active client sessions (API keys). + +### Session List + +| Column | Description | +|--------|-------------| +| **Hashed Key** | Privacy-preserving identifier (not the actual key) | +| **Balance** | Remaining funds in the session | +| **Spent** | Total amount spent by this session | +| **Requests** | Number of API calls made | +| **Created** | When the session was created | +| **Expires** | Auto-expiry time (if set) | + +### Actions + +- **View Details** — See full session history +- **Revoke** — Terminate a session (remaining balance returns to your wallet) + + + +--- + +## Models Tab + +Manage which AI models you offer to clients. + +### Model List + +Shows all models available from your upstream provider(s): + +| Column | Description | +|--------|-------------| +| **Model ID** | The model identifier (e.g., `gpt-4o`) | +| **Enabled** | Whether clients can use this model | +| **Input Price** | Cost per 1M input tokens (USD) | +| **Output Price** | Cost per 1M output tokens (USD) | +| **Custom** | Whether pricing is overridden | + +### Actions + +- **Import Models** — Fetch latest model list from upstream +- **Enable/Disable** — Toggle model availability +- **Edit Pricing** — Override default pricing for a model +- **Create Alias** — Map a friendly name to a model + +### Editing a Model + +Click on any model to configure: + +| Field | Description | +|-------|-------------| +| **Enabled** | Show this model to clients | +| **Prompt Price** | Custom price per 1M input tokens (USD) | +| **Completion Price** | Custom price per 1M output tokens (USD) | +| **Alias** | Alternative name for this model | + + + + +--- + +## Settings Tab + +Configure all node settings. Changes take effect immediately. + +### Upstream + +Connect to your AI provider: + +| Field | Description | +|-------|-------------| +| **Base URL** | API endpoint (e.g., `https://api.openai.com/v1`) | +| **API Key** | Your provider's secret key | + + + +### Node Identity + +| Field | Description | +|-------|-------------| +| **Name** | Public display name | +| **Description** | Brief description of your service | +| **Npub** | Nostr public key for discovery | + +### Pricing + +| Field | Description | +|-------|-------------| +| **Fixed Pricing** | Toggle flat-rate vs. per-token pricing | +| **Fixed Cost** | Sats per request (when fixed pricing enabled) | +| **Exchange Fee** | Multiplier for BTC volatility buffer | +| **Upstream Fee** | Your profit margin multiplier | + +**Example**: With Exchange Fee `1.005` and Upstream Fee `1.10`: + +- Upstream cost: $30/1M tokens +- Your price: $30 × 1.005 × 1.10 = $33.17/1M tokens + +### Cashu Mints + +Manage which mints you accept payments from: + +- **Add Mint** — Enter a mint URL +- **Remove Mint** — Stop accepting from a mint +- **Test Connection** — Verify mint is reachable + + + +### Lightning + +| Field | Description | +|-------|-------------| +| **Lightning Address** | Your LN address for automatic withdrawals | + +### Nostr Discovery + +| Field | Description | +|-------|-------------| +| **Nsec** | Private key for signing announcements | +| **Relays** | Where to publish your node advertisement | + +### Security + +| Field | Description | +|-------|-------------| +| **Admin Password** | Password for dashboard access | + +!!! warning "Set a Password" + The dashboard has no password by default. Always set one for production nodes. + + + +--- + +## Withdraw Tab + +Withdraw your profits to a Lightning wallet. + +### Steps + +1. **Select Mint** — Choose which mint to withdraw from +2. **Enter Amount** — How many sats to withdraw +3. **Generate Token** — Creates a Cashu token +4. **Redeem** — Paste the token into your Cashu wallet and melt to Lightning + + + +### Alternative: Lightning Address + +If you've configured a Lightning Address in Settings, profits can be automatically swept to your wallet (coming soon). + +--- + +## Logs Tab + +View node logs for debugging without SSH access. + +### Features + +- **Filter by Level** — Error, Warning, Info, Debug +- **Search** — Find specific entries +- **Time Range** — View logs from specific periods +- **Auto-refresh** — Watch logs in real-time + +### Common Log Entries + +| Entry | Meaning | +|-------|---------| +| `Upstream request failed` | Problem connecting to your AI provider | +| `Invalid token` | Client sent an invalid Cashu token | +| `Session expired` | API key reached its time limit | +| `Insufficient balance` | Client ran out of funds mid-request | + + diff --git a/docs/provider/deployment.md b/docs/provider/deployment.md new file mode 100644 index 0000000..4ad21e7 --- /dev/null +++ b/docs/provider/deployment.md @@ -0,0 +1,196 @@ +# Deployment + +Production deployment guide for Routstr Provider nodes. + +## Docker Compose (Recommended) + +For production, use Docker Compose with persistent storage and optional Tor support. + +### Basic Setup + +Create a `compose.yml`: + +```yaml +services: + routstr: + image: ghcr.io/routstr/proxy:latest + container_name: routstr + restart: unless-stopped + ports: + - "8000:8000" + volumes: + - ./data:/app/data + - ./logs:/app/logs +``` + +Start the node: + +```bash +docker compose up -d +``` + +Then configure everything via the [Admin Dashboard](http://localhost:8000/admin/). + +--- + +## With Tor (Anonymous Access) + +Add Tor to serve your node as a hidden service—no port forwarding needed. + +```yaml +services: + routstr: + image: ghcr.io/routstr/proxy:latest + container_name: routstr + restart: unless-stopped + ports: + - "8000:8000" + volumes: + - ./data:/app/data + - ./logs:/app/logs + environment: + - TOR_PROXY_URL=socks5://tor:9050 + depends_on: + - tor + + tor: + image: ghcr.io/hundehausen/tor-hidden-service:latest + container_name: tor + restart: unless-stopped + volumes: + - ./tor-data:/var/lib/tor + environment: + - HS_ROUTER=routstr:8000:80 +``` + +After starting, find your `.onion` address: + +```bash +docker exec tor cat /var/lib/tor/hidden_service/hostname +``` + +See [Tor Support](tor.md) for details. + +--- + +## Pre-Configuration (Optional) + +While everything can be configured via the dashboard, you can pre-configure settings with environment variables for automated deployments. + +### Using Environment Variables + +```yaml +services: + routstr: + image: ghcr.io/routstr/proxy:latest + environment: + # Pre-configure upstream (optional) + - UPSTREAM_BASE_URL=https://api.openai.com/v1 + - UPSTREAM_API_KEY=sk-proj-... + + # Secure the dashboard (recommended) + - ADMIN_PASSWORD=your-secure-password + + # Node identity + - NAME=My Provider Node + - DESCRIPTION=Fast GPT-4 access via Lightning + + # Lightning withdrawals + - RECEIVE_LN_ADDRESS=me@walletofsatoshi.com + volumes: + - ./data:/app/data +``` + +### Using an .env File + +```yaml +services: + routstr: + image: ghcr.io/routstr/proxy:latest + env_file: + - .env + volumes: + - ./data:/app/data +``` + +Example `.env`: + +```bash +UPSTREAM_BASE_URL=https://api.openai.com/v1 +UPSTREAM_API_KEY=sk-proj-... +ADMIN_PASSWORD=change-me +NAME=My Provider Node +RECEIVE_LN_ADDRESS=me@walletofsatoshi.com +``` + +See [Configuration](configuration.md) for all available options. + +--- + +## Persistence + +Routstr stores all data in `/app/data`: + +| Path | Contents | +|------|----------| +| `keys.db` | SQLite database (settings, API keys, sessions) | +| `.wallet/` | Cashu wallet data (your Bitcoin!) | + +!!! warning "Back Up Your Data" + The `./data` volume contains your wallet. Losing it means losing funds. Back up regularly. + +--- + +## Reverse Proxy (Optional) + +For custom domains and SSL, use a reverse proxy like Caddy or nginx. + +### Caddy Example + +``` +api.yournode.com { + reverse_proxy localhost:8000 +} +``` + +### nginx Example + +```nginx +server { + listen 443 ssl; + server_name api.yournode.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://localhost:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +--- + +## Updates + +Pull the latest image and restart: + +```bash +docker compose pull +docker compose up -d +``` + +--- + +## Building from Source + +```bash +git clone https://github.com/routstr/routstr-core.git +cd routstr-core +docker build -t routstr-local . +``` diff --git a/docs/provider/discovery.md b/docs/provider/discovery.md new file mode 100644 index 0000000..c0d0603 --- /dev/null +++ b/docs/provider/discovery.md @@ -0,0 +1,95 @@ +# Discovery + +Routstr uses **Nostr** as a decentralized directory for service discovery. Your node announces its presence, models, and pricing on Nostr relays, allowing clients to find you without a central server. + +--- + +## How It Works + +1. **Provider Advertisement (Kind 38421)**: Your node periodically publishes an event with its URL, models, and pricing +2. **Client Discovery**: Clients query relays for these events to find suitable providers + +--- + +## Configuration + +Configure discovery in **Dashboard** → **Settings** → **Nostr**. + +### Required Settings + +| Field | Description | +|-------|-------------| +| **Npub** | Your node's public identity (clients use this to verify your node) | +| **Nsec** | Your node's private key (used to sign advertisements) | +| **Relays** | Where to publish your announcements | + +### Default Relays + +If not configured, Routstr publishes to: + +- `wss://relay.damus.io` +- `wss://relay.nostr.band` +- `wss://nos.lol` + +--- + +## Advertisement Format + +Your node publishes events like: + +```json +{ + "kind": 38421, + "content": { + "name": "My Routstr Node", + "description": "Fast GPT-4 access via Lightning", + "endpoints": { + "http": "https://api.mynode.com", + "onion": "http://xyz...onion" + }, + "models": ["gpt-4", "claude-3-opus"], + "pricing": { ... } + }, + "tags": [ + ["d", "routstr-provider"], + ["g", "US"] + ] +} +``` + +--- + +## Tor Integration + +If you're running with Tor (see [Tor Support](tor.md)), your `.onion` address is automatically included in announcements. This allows clients to connect anonymously. + +--- + +## Verify Your Announcements + +Check if your node is broadcasting: + +1. Copy your `Npub` +2. Search on [Nostr.band](https://nostr.band) or [Primal](https://primal.net) +3. Look for Kind 38421 events + +--- + +## Generating Keys + +If you don't have a Nostr identity: + +1. Use any Nostr client (e.g., [Primal](https://primal.net), [Damus](https://damus.io)) +2. Create an account +3. Export your keys (npub and nsec) +4. Enter them in the dashboard + +Or generate keys programmatically: + +```python +from nostr_sdk import Keys + +keys = Keys.generate() +print(f"npub: {keys.public_key().to_bech32()}") +print(f"nsec: {keys.secret_key().to_bech32()}") +``` diff --git a/docs/provider/pricing.md b/docs/provider/pricing.md new file mode 100644 index 0000000..2907c69 --- /dev/null +++ b/docs/provider/pricing.md @@ -0,0 +1,91 @@ +# Pricing + +Routstr's pricing engine lets you act as a retailer of AI compute. You pay upstream providers (OpenAI, Anthropic, etc.) at their rates and sell to clients with your markup. + +--- + +## Pricing Strategies + +Configure these in **Dashboard** → **Settings** → **Pricing**. + +### Dynamic Pricing (Default) + +Passes through upstream costs plus your percentage markup. + +**Formula**: `Client Price = Upstream Cost × Exchange Fee × Upstream Fee` + +| Setting | Description | Default | +|---------|-------------|---------| +| **Exchange Fee** | Buffer for BTC price volatility | 1.005 (0.5%) | +| **Upstream Fee** | Your profit margin | 1.10 (10%) | + +**Example**: GPT-4 costs $30/1M tokens from OpenAI. With default settings: + +- Price: $30 × 1.005 × 1.10 = $33.17/1M tokens +- At $60k BTC: ~55,000 sats/1M tokens + +### Fixed Pricing + +Charge a flat rate per request, regardless of model or token count. + +| Setting | Description | +|---------|-------------| +| **Fixed Pricing** | Enable flat-rate mode | +| **Fixed Cost** | Sats per request | + +**Best for**: Simple proxies, internal tools, or subscription-like access. + +--- + +## Per-Model Pricing + +Override pricing for specific models in **Dashboard** → **Models**. + +1. Click on a model +2. Enter custom **Prompt Price** and **Completion Price** (USD per 1M tokens) +3. Save + +This overrides both the upstream cost and your global markup for that model. + +**Example**: Lock GPT-4 at $35/1M tokens regardless of OpenAI's actual rate or your fee settings. + +--- + +## Token-Based Overrides + +Set global fixed rates per token (overrides dynamic pricing for all models): + +| Setting | Description | +|---------|-------------| +| **Fixed Per 1K Input** | Sats per 1,000 prompt tokens | +| **Fixed Per 1K Output** | Sats per 1,000 completion tokens | + +--- + +## Minimum Charge + +Prevent spam with a minimum cost per request: + +| Setting | Description | Default | +|---------|-------------|---------| +| **Min Request Cost** | Minimum charge in msats | 1000 (1 sat) | + +If a request's calculated cost is lower than this, the client pays the minimum instead. + +--- + +## Cost Tracking + +Routstr tracks balances in **millisats (msats)** for precision with cheap models. + +- 1 sat = 1,000 msats +- API responses include cost in msats +- Lightning withdrawals round down to whole sats + +### Client Verification (RIP-05) + +Clients can verify charges: + +1. Fetch `/v1/models` for your advertised rates +2. Calculate expected cost from token counts +3. Compare to `x-routstr-cost` response header diff --git a/docs/provider/quickstart.md b/docs/provider/quickstart.md new file mode 100644 index 0000000..2771c1f --- /dev/null +++ b/docs/provider/quickstart.md @@ -0,0 +1,99 @@ +# Quick Start + +Start earning Bitcoin by selling AI access in under 5 minutes. + +## What You'll Build + +A **Routstr Provider Node** acts as a gateway that: + +1. **Connects** to upstream AI providers (OpenAI, Anthropic, OpenRouter, etc.) +2. **Accepts** Bitcoin payments via Cashu eCash +3. **Serves** AI requests to clients on the network + +You bring the API keys, Routstr handles the billing, payments, and client management. + +!!! tip "Future: Node-to-Node Routing" + In future versions, you'll be able to run a node that connects to other Routstr nodes—eliminating the need to configure upstream providers yourself. For now, you'll need your own API credentials. + +--- + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) installed +- API credentials from at least one AI provider (OpenAI, Anthropic, OpenRouter, etc.) + +--- + +## 1. Start the Node + +```bash +docker run -d \ + --name routstr \ + -p 8000:8000 \ + -v routstr-data:/app/data \ + ghcr.io/routstr/proxy:latest +``` + +Verify it's running: + +```bash +curl http://localhost:8000/v1/info +``` + +--- + +## 2. Configure via Dashboard + +Open the **Admin Dashboard** at [http://localhost:8000/admin/](http://localhost:8000/admin/). + +!!! note "Default Access" + The dashboard has no password by default. Set one immediately in Settings for production use. + +### Connect Your AI Providers + +1. Navigate to **Settings** → **Upstream** +2. Enter your upstream URL (e.g., `https://api.openai.com/v1`) +3. Enter your API key +4. Save + +### Set Your Profit Margin + +1. Go to **Settings** → **Pricing** +2. Configure your markup (default is 10%) +3. Optionally set a fixed price per request instead + +### Secure the Dashboard + +1. Go to **Settings** → **Admin** +2. Set a strong password +3. Save and re-login + +--- + +## 3. Start Earning + +Once configured, your node is live. Clients pay you in Bitcoin (via Cashu tokens) for every AI request. + +### Monitor Your Earnings + +The dashboard shows: + +- **Total Wallet**: All Bitcoin held by your node +- **User Balances**: Funds belonging to active client sessions +- **Your Balance**: Your profit (`Total - User Balances`) + +### Withdraw Profits + +1. Go to **Withdraw** in the dashboard +2. Select amount and mint +3. Generate a Cashu token +4. Redeem to your Lightning wallet + +--- + +## Next Steps + +- **[Deployment](deployment.md)**: Production setup with Docker Compose and Tor +- **[Dashboard Guide](dashboard.md)**: Full reference for all dashboard features +- **[Pricing](pricing.md)**: Configure pricing strategies and per-model overrides +- **[Discovery](discovery.md)**: Announce your node on Nostr for clients to find you diff --git a/docs/provider/tor.md b/docs/provider/tor.md new file mode 100644 index 0000000..187e4c5 --- /dev/null +++ b/docs/provider/tor.md @@ -0,0 +1,50 @@ +# Tor Support + +Running Routstr as a **Tor Hidden Service** allows you to offer API access anonymously and bypass NAT/firewalls without port forwarding. + +## Automatic Setup (Docker) + +The standard `compose.yml` includes a Tor container pre-configured to serve your node. + +1. **Start the stack**: `docker compose up -d` +2. **Wait**: Tor takes about 30 seconds to generate keys and bootstrap. +3. **Find your address**: + ```bash + docker exec tor cat /var/lib/tor/hidden_service/hostname + ``` + Output: `v2xyz...longaddress.onion` + +Routstr will automatically detect this address (via the `discover_onion_url_from_tor` logic) and include it in: +- The `/v1/info` endpoint. +- Nostr announcements (RIP-02). + +## Manual Setup + +If you are running outside Docker or managing Tor yourself: + +1. **Install Tor**: `sudo apt install tor` +2. **Edit `torrc`**: + ``` + HiddenServiceDir /var/lib/tor/routstr/ + HiddenServicePort 80 127.0.0.1:8000 + ``` +3. **Restart Tor**: `sudo systemctl restart tor` +4. **Get Address**: `sudo cat /var/lib/tor/routstr/hostname` +5. **Configure Routstr**: + Set `ONION_URL=http://youraddress.onion` in your `.env` file so the node knows its own address. + +## Client Usage + +Clients connecting to your `.onion` address must route traffic through SOCKS5. + +**Python Example:** +```python +import httpx +from openai import OpenAI + +client = OpenAI( + base_url="http://youraddress.onion/v1", + api_key="sk-...", + http_client=httpx.Client(proxy="socks5://127.0.0.1:9050") +) +``` diff --git a/mkdocs.yml b/mkdocs.yml index b8cbfaa..9221c71 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,29 +77,27 @@ extra: nav: - Home: index.md - - Getting Started: - - Overview: getting-started/overview.md - - Quick Start: getting-started/quickstart.md - - Docker Setup: getting-started/docker.md - - Configuration: getting-started/configuration.md - - User Guide: - - Introduction: user-guide/introduction.md - - Payment Flow: user-guide/payment-flow.md - - Using the API: user-guide/using-api.md - - Admin Dashboard: user-guide/admin-dashboard.md - - Models & Pricing: user-guide/models-pricing.md - - Contributing: - - Setup Development: contributing/setup.md - - Architecture: contributing/architecture.md - - Code Structure: contributing/code-structure.md - - Testing: contributing/testing.md + - Overview: overview.md + - Client Guide: + - Introduction: client/introduction.md + - Payment Flow: client/payments.md + - Integration: client/integration.md + - Provider Guide: + - Quick Start: provider/quickstart.md + - Dashboard: provider/dashboard.md + - Deployment: provider/deployment.md + - Configuration: provider/configuration.md + - Pricing: provider/pricing.md + - Advanced Pricing: provider/advanced-pricing.md + - Discovery: provider/discovery.md + - Tor Support: provider/tor.md - API Reference: - Overview: api/overview.md - Authentication: api/authentication.md - Endpoints: api/endpoints.md - Errors: api/errors.md - - Advanced: - - Tor Support: advanced/tor.md - - Nostr Discovery: advanced/nostr.md - - Custom Pricing: advanced/custom-pricing.md - - Migrations: advanced/migrations.md + - Contributing: + - Setup Development: contributing/setup.md + - Architecture: contributing/architecture.md + - Code Structure: contributing/code-structure.md + - Testing: contributing/testing.md