mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-22 12:22:20 +00:00
improved docs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
143
docs/client/integration.md
Normal file
143
docs/client/integration.md
Normal file
@@ -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),
|
||||
)
|
||||
```
|
||||
142
docs/client/introduction.md
Normal file
142
docs/client/introduction.md
Normal file
@@ -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.
|
||||
97
docs/client/payments.md
Normal file
97
docs/client/payments.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
|
||||
@@ -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! 🚀
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
- **[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
|
||||
|
||||
</div>
|
||||
|
||||
## 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).*
|
||||
|
||||
76
docs/overview.md
Normal file
76
docs/overview.md
Normal file
@@ -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<br/>Proxy + Auth + Billing]
|
||||
end
|
||||
|
||||
subgraph Upstream
|
||||
C[OpenAI / Anthropic<br/>vLLM / Ollama / ...]
|
||||
end
|
||||
|
||||
A -- "Request +<br/>Cashu Token" --> B
|
||||
B -- "Forward<br/>Request" --> C
|
||||
C -- "Response +<br/>Usage" --> B
|
||||
B -- "Response +<br/>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) |
|
||||
114
docs/provider/advanced-pricing.md
Normal file
114
docs/provider/advanced-pricing.md
Normal file
@@ -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
|
||||
122
docs/provider/configuration.md
Normal file
122
docs/provider/configuration.md
Normal file
@@ -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.
|
||||
208
docs/provider/dashboard.md
Normal file
208
docs/provider/dashboard.md
Normal file
@@ -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
|
||||
|
||||
<!-- TODO: Screenshot of Overview tab -->
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
<!-- TODO: Screenshot of Sessions tab -->
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|
||||
<!-- TODO: Screenshot of Models tab -->
|
||||
<!-- TODO: Screenshot of Model edit modal -->
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|
||||
<!-- TODO: Screenshot of Upstream settings -->
|
||||
|
||||
### 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
|
||||
|
||||
<!-- TODO: Screenshot of Mints settings -->
|
||||
|
||||
### 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.
|
||||
|
||||
<!-- TODO: Screenshot of Security settings -->
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
<!-- TODO: Screenshot of Withdraw tab -->
|
||||
|
||||
### 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 |
|
||||
|
||||
<!-- TODO: Screenshot of Logs tab -->
|
||||
196
docs/provider/deployment.md
Normal file
196
docs/provider/deployment.md
Normal file
@@ -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 .
|
||||
```
|
||||
95
docs/provider/discovery.md
Normal file
95
docs/provider/discovery.md
Normal file
@@ -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()}")
|
||||
```
|
||||
91
docs/provider/pricing.md
Normal file
91
docs/provider/pricing.md
Normal file
@@ -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
|
||||
99
docs/provider/quickstart.md
Normal file
99
docs/provider/quickstart.md
Normal file
@@ -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
|
||||
50
docs/provider/tor.md
Normal file
50
docs/provider/tor.md
Normal file
@@ -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")
|
||||
)
|
||||
```
|
||||
40
mkdocs.yml
40
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
|
||||
|
||||
Reference in New Issue
Block a user