mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +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
|
- [Endpoints](endpoints.md) - Complete endpoint reference
|
||||||
- [Errors](errors.md) - Error handling guide
|
- [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
|
- [Errors](errors.md) - Error handling reference
|
||||||
- [Authentication](authentication.md) - Auth details
|
- [Authentication](authentication.md) - Auth details
|
||||||
- [Examples](../user-guide/using-api.md) - Code examples
|
- [Integration Guide](../client/integration.md) - Code examples
|
||||||
|
|||||||
+1
-1
@@ -589,4 +589,4 @@ class ErrorMetrics:
|
|||||||
|
|
||||||
- [Authentication](authentication.md) - Auth error details
|
- [Authentication](authentication.md) - Auth error details
|
||||||
- [Endpoints](endpoints.md) - Endpoint-specific errors
|
- [Endpoints](endpoints.md) - Endpoint-specific errors
|
||||||
- [Examples](../user-guide/using-api.md) - Error handling examples
|
- [Integration Guide](../client/integration.md) - Error handling examples
|
||||||
|
|||||||
+36
-89
@@ -99,19 +99,19 @@ All errors follow a consistent format:
|
|||||||
|
|
||||||
Standard OpenAI-compatible endpoints:
|
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`
|
- **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
|
### Payment Endpoints
|
||||||
|
|
||||||
Routstr-specific payment management:
|
Routstr-specific payment management:
|
||||||
|
|
||||||
- **Wallet**: `/v1/wallet/*`
|
- **Balance**: `/v1/balance/*`
|
||||||
- **Balance**: `/v1/balance`
|
|
||||||
- **Node Info**: `/v1/info`
|
- **Node Info**: `/v1/info`
|
||||||
|
|
||||||
### Admin Endpoints
|
### Admin Endpoints
|
||||||
@@ -137,9 +137,25 @@ Protected administrative functions:
|
|||||||
|
|
||||||
| Header | Description |
|
| Header | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `X-Routstr-Version` | API version override |
|
|
||||||
| `X-Cashu` | eCash token for per-request payment |
|
| `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
|
## Response Headers
|
||||||
|
|
||||||
@@ -149,16 +165,8 @@ Protected administrative functions:
|
|||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `Content-Type` | Response format |
|
| `Content-Type` | Response format |
|
||||||
| `Content-Length` | Response size |
|
| `Content-Length` | Response size |
|
||||||
| `X-Routstr-Request-ID` | Unique request identifier |
|
| `X-Request-ID` | Unique request identifier |
|
||||||
| `X-Routstr-Version` | API version used |
|
| `X-Cashu` | Change token (when request used `X-Cashu` header) |
|
||||||
|
|
||||||
### Cost Headers
|
|
||||||
|
|
||||||
| Header | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| `X-Routstr-Cost` | Request cost in sats |
|
|
||||||
| `X-Routstr-Balance` | Remaining balance |
|
|
||||||
| `X-Cashu` | Change token (if applicable) |
|
|
||||||
|
|
||||||
## Streaming Responses
|
## Streaming Responses
|
||||||
|
|
||||||
@@ -245,8 +253,6 @@ X-Webhook-Signature: sha256=...
|
|||||||
|
|
||||||
- Current version: `v1`
|
- Current version: `v1`
|
||||||
- Version in URL path: `/v1/endpoint`
|
- Version in URL path: `/v1/endpoint`
|
||||||
- Override with header: `X-Routstr-Version: v2`
|
|
||||||
- Deprecation notices: 6 months
|
|
||||||
|
|
||||||
## Status Codes
|
## Status Codes
|
||||||
|
|
||||||
@@ -284,82 +290,23 @@ Responses are compressed with gzip when:
|
|||||||
- Response is larger than 1KB
|
- Response is larger than 1KB
|
||||||
- Content type is compressible
|
- 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:
|
Supported models and pricing are available at `/v1/models`.
|
||||||
|
|
||||||
```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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [Authentication](authentication.md) - Detailed auth guide
|
- [Authentication](authentication.md) - Detailed auth guide
|
||||||
- [Endpoints](endpoints.md) - Complete endpoint reference
|
- [Endpoints](endpoints.md) - Complete endpoint reference
|
||||||
- [Errors](errors.md) - Error handling guide
|
- [Errors](errors.md) - Error handling guide
|
||||||
- [Examples](../user-guide/using-api.md) - Code examples
|
- [Integration Guide](../client/integration.md) - Code examples
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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
|
## 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
|
```mermaid
|
||||||
graph TB
|
graph TB
|
||||||
@@ -20,7 +20,7 @@ graph TB
|
|||||||
Auth[Auth Module]
|
Auth[Auth Module]
|
||||||
Payment[Payment Module]
|
Payment[Payment Module]
|
||||||
Proxy[Proxy Module]
|
Proxy[Proxy Module]
|
||||||
DB[(SQLite DB)]
|
DB[(SQLModel DB)]
|
||||||
|
|
||||||
API --> Auth
|
API --> Auth
|
||||||
Auth --> Payment
|
Auth --> Payment
|
||||||
@@ -40,57 +40,87 @@ graph TB
|
|||||||
|
|
||||||
The main application is initialized in `routstr/core/main.py`:
|
The main application is initialized in `routstr/core/main.py`:
|
||||||
|
|
||||||
- **Lifespan Management**: Handles startup/shutdown tasks
|
- **Lifespan Management**: Runs migrations, initializes DB, refreshes pricing/models, starts background tasks
|
||||||
- **Middleware**: CORS, logging, error handling
|
- **Middleware**: CORS and request logging
|
||||||
- **Routers**: Modular endpoint organization
|
- **Routers**: Admin, pricing/models, balance/wallet, providers discovery, proxy
|
||||||
- **Background Tasks**: Price updates, automatic payouts
|
- **Background Tasks**: Price refresh, model map refresh, payouts, node announcements, provider discovery refresh
|
||||||
|
|
||||||
### Authentication System
|
### Authentication System
|
||||||
|
|
||||||
Located in `routstr/auth.py`, handles:
|
Located in `routstr/auth.py`, handles:
|
||||||
|
|
||||||
- **API Key Validation**: Hashed key storage and lookup
|
- **API Key Validation**: SHA-256 hashed key lookup and persistence
|
||||||
- **Balance Checking**: Ensures sufficient funds
|
- **Balance Checking**: Ensures sufficient funds before requests
|
||||||
- **Rate Limiting**: Optional request throttling
|
- **Token Redemption**: Converts Cashu tokens to balance
|
||||||
- **Token Redemption**: Converts eCash to balance
|
|
||||||
|
|
||||||
### Payment Processing
|
### Payment Processing
|
||||||
|
|
||||||
The `routstr/payment/` module manages:
|
The `routstr/payment/` module manages:
|
||||||
|
|
||||||
- **Cost Calculation**: Token-based or fixed pricing
|
- **Cost Calculation**: Token-based or fixed pricing
|
||||||
- **Model Pricing**: Dynamic pricing from models.json
|
- **Model Pricing**: Derived from upstream providers and DB overrides
|
||||||
- **Currency Conversion**: BTC/USD rate management
|
- **Currency Conversion**: BTC/USD price refresh and conversion
|
||||||
- **Fee Application**: Exchange and provider fees
|
- **Fee Application**: Provider fee applied to upstream model pricing
|
||||||
|
|
||||||
### Request Proxying
|
### Request Proxying
|
||||||
|
|
||||||
`routstr/proxy.py` handles:
|
`routstr/proxy.py` handles:
|
||||||
|
|
||||||
- **Request Forwarding**: Preserves headers and body
|
- **Request Forwarding**: Forwards requests to selected upstream providers
|
||||||
- **Response Streaming**: Efficient memory usage
|
- **Response Streaming**: Streaming and non-streaming paths
|
||||||
- **Usage Tracking**: Counts tokens and costs
|
- **Usage Tracking**: Adjusts costs after upstream responses
|
||||||
- **Error Handling**: Graceful upstream failures
|
- **Error Handling**: Maps upstream errors to consistent responses
|
||||||
|
|
||||||
### Database Layer
|
### Database Layer
|
||||||
|
|
||||||
Using SQLModel in `routstr/core/db.py`:
|
Using SQLModel in `routstr/core/db.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Core models
|
# Core tables
|
||||||
APIKey:
|
ApiKey:
|
||||||
- id: Primary key
|
- hashed_key: Primary key (SHA-256 of key or Cashu token)
|
||||||
- key_hash: Hashed API key
|
|
||||||
- balance: Current balance (msats)
|
- balance: Current balance (msats)
|
||||||
- created_at: Timestamp
|
- reserved_balance: Reserved balance (msats)
|
||||||
- metadata: JSON field
|
- 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
|
- id: Primary key
|
||||||
- api_key_id: Foreign key
|
- provider_type: openai/anthropic/azure/openrouter/etc.
|
||||||
- amount: Transaction amount
|
- base_url: Provider API base URL
|
||||||
- type: deposit/usage/withdrawal
|
- api_key: Provider API key
|
||||||
- timestamp: When occurred
|
- 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
|
## Request Flow
|
||||||
@@ -107,10 +137,10 @@ sequenceDiagram
|
|||||||
C->>R: API Request + Key
|
C->>R: API Request + Key
|
||||||
R->>D: Validate Key
|
R->>D: Validate Key
|
||||||
D-->>R: Key Info + Balance
|
D-->>R: Key Info + Balance
|
||||||
R->>R: Check Balance
|
R->>R: Reserve Max Cost
|
||||||
R->>P: Forward Request
|
R->>P: Forward Request
|
||||||
P-->>R: AI Response
|
P-->>R: AI Response
|
||||||
R->>D: Deduct Cost
|
R->>D: Finalize Cost (adjust by usage)
|
||||||
R-->>C: Return Response
|
R-->>C: Return Response
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -124,36 +154,20 @@ sequenceDiagram
|
|||||||
participant M as Cashu Mint
|
participant M as Cashu Mint
|
||||||
participant D as Database
|
participant D as Database
|
||||||
|
|
||||||
C->>R: Create Key Request + Token
|
C->>R: Request + Cashu Token
|
||||||
R->>W: Validate Token
|
R->>W: Redeem Token
|
||||||
W->>M: Verify with Mint
|
W->>M: Verify with Mint
|
||||||
M-->>W: Token Valid
|
M-->>W: Token Valid
|
||||||
W-->>R: Token Amount
|
W-->>R: Token Amount
|
||||||
R->>D: Create Key + Balance
|
R->>D: Create/Update Key + Balance
|
||||||
R-->>C: Return API Key
|
R-->>C: Continue Request
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Design Decisions
|
## Key Design Decisions
|
||||||
|
|
||||||
### 1. Async Architecture
|
### 1. Async Architecture
|
||||||
|
|
||||||
Everything is async for maximum performance:
|
The system is async end-to-end, with background tasks for pricing refresh, provider discovery, model map refresh, and payouts.
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Modular Design
|
### 2. Modular Design
|
||||||
|
|
||||||
@@ -166,82 +180,46 @@ Components are loosely coupled:
|
|||||||
|
|
||||||
### 3. Error Handling
|
### 3. Error Handling
|
||||||
|
|
||||||
Graceful degradation and clear error messages:
|
Exceptions are handled by FastAPI exception handlers to return consistent JSON responses with a request ID.
|
||||||
|
|
||||||
```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"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Database Migrations
|
### 4. Database Migrations
|
||||||
|
|
||||||
Using Alembic for schema management:
|
Alembic migrations are run on startup, and tables are created for any models not tracked by migrations.
|
||||||
|
|
||||||
- Auto-migrations on startup
|
|
||||||
- Version control for schema changes
|
|
||||||
- Rollback capability
|
|
||||||
- Zero-downtime updates
|
|
||||||
|
|
||||||
## Security Architecture
|
## Security Architecture
|
||||||
|
|
||||||
### API Key Security
|
### API Key Security
|
||||||
|
|
||||||
- **Storage**: SHA-256 hashed keys
|
- **Storage**: SHA-256 hashed keys
|
||||||
- **Generation**: Cryptographically secure random
|
- **Generation**: Cryptographically secure random (when creating new keys)
|
||||||
- **Validation**: Constant-time comparison
|
- **Validation**: Hash lookup in DB
|
||||||
- **Rotation**: Support for key expiry
|
- **Expiry**: Optional refund flow via `key_expiry_time` and `refund_address`
|
||||||
|
|
||||||
### Payment Security
|
### Payment Security
|
||||||
|
|
||||||
- **Token Validation**: Cryptographic verification
|
- **Token Validation**: Cashu token redemption via mint
|
||||||
- **Double-Spend Prevention**: Mint verification
|
- **Balance Protection**: Atomic updates and reserved balance tracking
|
||||||
- **Balance Protection**: Atomic transactions
|
- **Audit Trail**: Structured logging of payments and adjustments
|
||||||
- **Audit Trail**: All transactions logged
|
|
||||||
|
|
||||||
### Network Security
|
### Network Security
|
||||||
|
|
||||||
- **HTTPS**: Enforced in production
|
|
||||||
- **CORS**: Configurable origins
|
- **CORS**: Configurable origins
|
||||||
- **Rate Limiting**: Per-key limits
|
|
||||||
- **Input Validation**: Pydantic models
|
- **Input Validation**: Pydantic models
|
||||||
|
|
||||||
## Performance Considerations
|
## Performance Considerations
|
||||||
|
|
||||||
### Caching Strategy
|
### Caching Strategy
|
||||||
|
|
||||||
```python
|
Model and provider selections are cached in process memory and refreshed on a schedule.
|
||||||
# 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)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Optimization
|
### Database Optimization
|
||||||
|
|
||||||
- **Connection Pooling**: Reuse connections
|
|
||||||
- **Indexed Queries**: Key lookups are O(1)
|
|
||||||
- **Batch Operations**: Group updates
|
|
||||||
- **Async I/O**: Non-blocking queries
|
- **Async I/O**: Non-blocking queries
|
||||||
|
- **Atomic Updates**: Balance reservation and finalization updates
|
||||||
|
|
||||||
### Streaming Responses
|
### Streaming Responses
|
||||||
|
|
||||||
Efficient memory usage for large responses:
|
Streaming responses are forwarded from upstream providers with usage tracking hooks.
|
||||||
|
|
||||||
```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)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Extension Points
|
## Extension Points
|
||||||
|
|
||||||
@@ -273,8 +251,6 @@ async def stream_response(upstream_response):
|
|||||||
|
|
||||||
- Mock external dependencies
|
- Mock external dependencies
|
||||||
- Test business logic in isolation
|
- Test business logic in isolation
|
||||||
- Fast execution (< 1 second per test)
|
|
||||||
- High coverage target (> 80%)
|
|
||||||
|
|
||||||
### Integration Tests
|
### Integration Tests
|
||||||
|
|
||||||
@@ -285,10 +261,7 @@ async def stream_response(upstream_response):
|
|||||||
|
|
||||||
### Performance Tests
|
### Performance Tests
|
||||||
|
|
||||||
- Load testing with locust
|
- Response time benchmarks (as needed)
|
||||||
- Memory profiling
|
|
||||||
- Database query optimization
|
|
||||||
- Response time benchmarks
|
|
||||||
|
|
||||||
## Monitoring and Observability
|
## Monitoring and Observability
|
||||||
|
|
||||||
@@ -308,41 +281,17 @@ logger.info("api_request", extra={
|
|||||||
|
|
||||||
### Metrics Collection
|
### Metrics Collection
|
||||||
|
|
||||||
Key metrics tracked:
|
Structured logs are emitted for requests, pricing, and payment events.
|
||||||
|
|
||||||
- Request rate by endpoint
|
|
||||||
- Token usage by model
|
|
||||||
- Balance changes
|
|
||||||
- Error rates
|
|
||||||
- Response times
|
|
||||||
|
|
||||||
### Health Checks
|
### Health Checks
|
||||||
|
|
||||||
```python
|
Use `/v1/info` for basic service metadata and configuration visibility.
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
return {
|
|
||||||
"status": "healthy",
|
|
||||||
"version": __version__,
|
|
||||||
"database": await check_db(),
|
|
||||||
"upstream": await check_upstream(),
|
|
||||||
"mints": await check_mints()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Architecture
|
## Deployment Architecture
|
||||||
|
|
||||||
### Container Structure
|
### Container Structure
|
||||||
|
|
||||||
```dockerfile
|
See `core/Dockerfile` for the current container build configuration.
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Configuration
|
### Environment Configuration
|
||||||
|
|
||||||
@@ -352,10 +301,9 @@ FROM python:3.11-slim
|
|||||||
|
|
||||||
### Scaling Considerations
|
### Scaling Considerations
|
||||||
|
|
||||||
- **Horizontal**: Multiple instances behind load balancer
|
- **Horizontal**: Multiple instances behind a load balancer
|
||||||
- **Vertical**: Async handles high concurrency
|
- **Vertical**: Async handles high concurrency
|
||||||
- **Database**: Consider PostgreSQL for scale
|
- **Database**: Configure `DATABASE_URL` for external databases
|
||||||
- **Caching**: Redis for distributed cache
|
|
||||||
|
|
||||||
## Future Architecture
|
## Future Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ This guide provides a detailed overview of Routstr Core's codebase organization
|
|||||||
```
|
```
|
||||||
routstr-core/
|
routstr-core/
|
||||||
├── routstr/ # Main application package
|
├── routstr/ # Main application package
|
||||||
│ ├── __init__.py # Package initialization, loads .env
|
│ ├── __init__.py # Package initialization, exports FastAPI app
|
||||||
│ ├── auth.py # Authentication and authorization
|
│ ├── algorithm.py # Model selection/mapping logic
|
||||||
|
│ ├── auth.py # Bearer/Cashu auth and payment handling
|
||||||
│ ├── balance.py # Balance management endpoints
|
│ ├── balance.py # Balance management endpoints
|
||||||
│ ├── discovery.py # Nostr relay discovery
|
│ ├── discovery.py # Nostr relay discovery
|
||||||
|
│ ├── lightning.py # Lightning invoice topups
|
||||||
|
│ ├── nip91.py # Node announcement logic
|
||||||
│ ├── proxy.py # Request proxying logic
|
│ ├── proxy.py # Request proxying logic
|
||||||
│ ├── wallet.py # Cashu wallet operations
|
│ ├── wallet.py # Cashu wallet operations
|
||||||
│ │
|
│ │
|
||||||
@@ -18,19 +21,23 @@ routstr-core/
|
|||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ ├── admin.py # Admin dashboard and API
|
│ │ ├── admin.py # Admin dashboard and API
|
||||||
│ │ ├── db.py # Database models and connection
|
│ │ ├── db.py # Database models and connection
|
||||||
│ │ ├── exceptions.py # Custom exception classes
|
│ │ ├── exceptions.py # Exception handlers
|
||||||
│ │ ├── logging.py # Structured logging setup
|
│ │ ├── logging.py # Structured logging setup
|
||||||
│ │ ├── main.py # FastAPI app initialization
|
│ │ ├── main.py # FastAPI app initialization
|
||||||
│ │ └── middleware.py # HTTP middleware components
|
│ │ └── middleware.py # HTTP middleware components
|
||||||
│ │
|
│ │
|
||||||
│ └── payment/ # Payment processing
|
│ ├── payment/ # Payment processing
|
||||||
│ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ ├── cost_calculation.py # Usage cost calculation
|
│ │ ├── cost_calculation.py # Usage cost calculation
|
||||||
│ ├── helpers.py # Payment utilities
|
│ │ ├── helpers.py # Payment utilities
|
||||||
│ ├── lnurl.py # Lightning URL support
|
│ │ ├── lnurl.py # Lightning URL support
|
||||||
│ ├── models.py # Model pricing management
|
│ │ ├── models.py # Model pricing management
|
||||||
│ ├── price.py # BTC/USD price handling
|
│ │ └── price.py # BTC/USD price handling
|
||||||
│ └── x_cashu.py # Cashu header protocol
|
│ │
|
||||||
|
│ └── upstream/ # Upstream provider integrations
|
||||||
|
│ ├── base.py # Base provider logic
|
||||||
|
│ ├── helpers.py # Provider init and model refresh
|
||||||
|
│ └── ... # Provider implementations
|
||||||
│
|
│
|
||||||
├── tests/ # Test suite
|
├── tests/ # Test suite
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
@@ -45,7 +52,12 @@ routstr-core/
|
|||||||
│ └── versions/ # Migration files
|
│ └── versions/ # Migration files
|
||||||
│
|
│
|
||||||
├── scripts/ # Utility scripts
|
├── 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
|
├── docs/ # Documentation
|
||||||
├── logs/ # Application logs (git ignored)
|
├── logs/ # Application logs (git ignored)
|
||||||
@@ -71,30 +83,27 @@ routstr-core/
|
|||||||
#### `routstr/__init__.py`
|
#### `routstr/__init__.py`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Loads environment variables
|
|
||||||
import dotenv
|
|
||||||
dotenv.load_dotenv()
|
|
||||||
|
|
||||||
# Exports FastAPI app
|
|
||||||
from .core.main import app as fastapi_app
|
from .core.main import app as fastapi_app
|
||||||
|
|
||||||
|
__all__ = ["fastapi_app"]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `routstr/core/main.py`
|
#### `routstr/core/main.py`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# FastAPI application setup
|
# FastAPI application setup
|
||||||
app = FastAPI(
|
app = FastAPI(version=__version__, lifespan=lifespan)
|
||||||
title="Routstr Node",
|
|
||||||
lifespan=lifespan, # Manages startup/shutdown
|
|
||||||
)
|
|
||||||
|
|
||||||
# Middleware registration
|
# Middleware registration
|
||||||
app.add_middleware(CORSMiddleware, ...)
|
app.add_middleware(CORSMiddleware, ...)
|
||||||
app.add_middleware(LoggingMiddleware)
|
app.add_middleware(LoggingMiddleware)
|
||||||
|
|
||||||
# Router inclusion
|
# Router inclusion
|
||||||
|
app.include_router(models_router)
|
||||||
app.include_router(admin_router)
|
app.include_router(admin_router)
|
||||||
app.include_router(balance_router)
|
app.include_router(balance_router)
|
||||||
|
app.include_router(deprecated_wallet_router)
|
||||||
|
app.include_router(providers_router)
|
||||||
app.include_router(proxy_router)
|
app.include_router(proxy_router)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -102,29 +111,24 @@ app.include_router(proxy_router)
|
|||||||
|
|
||||||
#### `routstr/auth.py`
|
#### `routstr/auth.py`
|
||||||
|
|
||||||
Handles API key validation and authorization:
|
Handles bearer key validation and payment lifecycle (bearer or Cashu token):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class APIKeyAuth:
|
async def validate_bearer_key(
|
||||||
"""FastAPI dependency for API key authentication"""
|
bearer_key: str,
|
||||||
|
session: AsyncSession,
|
||||||
async def __call__(self, request: Request) -> APIKey:
|
refund_address: Optional[str] = None,
|
||||||
# Extract and validate API key
|
key_expiry_time: Optional[int] = None,
|
||||||
# Check balance
|
) -> ApiKey:
|
||||||
# Return authenticated key object
|
"""Validate bearer API key or redeem Cashu token into a balance."""
|
||||||
|
|
||||||
# Usage in routes:
|
|
||||||
@router.get("/protected")
|
|
||||||
async def protected_route(api_key: APIKey = Depends(APIKeyAuth())):
|
|
||||||
pass
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Key functions:
|
Key functions:
|
||||||
|
|
||||||
- `create_api_key()` - Generate new API keys
|
- `validate_bearer_key()` - Validate API key or Cashu token
|
||||||
- `validate_api_key()` - Verify and retrieve key
|
- `pay_for_request()` - Reserve max cost before upstream call
|
||||||
- `check_balance()` - Ensure sufficient funds
|
- `adjust_payment_for_tokens()` - Adjust final cost after response
|
||||||
- `update_last_used()` - Track usage
|
- `revert_pay_for_request()` - Refund on upstream failure
|
||||||
|
|
||||||
### Payment Processing
|
### Payment Processing
|
||||||
|
|
||||||
@@ -133,56 +137,30 @@ Key functions:
|
|||||||
Calculates request costs:
|
Calculates request costs:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def calculate_request_cost(
|
async def calculate_cost(
|
||||||
model: str,
|
response_data: dict, max_cost: int, session: AsyncSession
|
||||||
prompt_tokens: int,
|
) -> CostData | MaxCostData | CostDataError:
|
||||||
completion_tokens: int,
|
"""Calculate cost in millisatoshis from response usage or model pricing."""
|
||||||
**kwargs
|
|
||||||
) -> CostData:
|
|
||||||
"""Calculate cost in millisatoshis"""
|
|
||||||
# Model-based or fixed pricing
|
|
||||||
# Token counting
|
|
||||||
# Fee application
|
|
||||||
# Currency conversion
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `routstr/payment/models.py`
|
#### `routstr/payment/models.py`
|
||||||
|
|
||||||
Manages model pricing data:
|
Manages model pricing, database overrides, and pricing refresh:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class ModelPrice:
|
class Model(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
pricing: dict[str, float] # USD prices
|
pricing: Pricing
|
||||||
context_length: int
|
sats_pricing: Pricing | None = None
|
||||||
|
|
||||||
# Global model registry
|
|
||||||
MODELS: dict[str, ModelPrice] = load_models()
|
|
||||||
|
|
||||||
# Dynamic price updates
|
|
||||||
async def update_sats_pricing():
|
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:
|
The `x-cashu` header is handled by the proxy route and delegated to upstream providers.
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request Proxying
|
### Request Proxying
|
||||||
|
|
||||||
@@ -191,17 +169,11 @@ class XCashuHandler:
|
|||||||
Core proxy functionality:
|
Core proxy functionality:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@router.api_route("/{path:path}", methods=ALL_METHODS)
|
@proxy_router.api_route("/{path:path}", methods=["GET", "POST"], response_model=None)
|
||||||
async def proxy_request(
|
async def proxy(
|
||||||
request: Request,
|
request: Request, path: str, session: AsyncSession = Depends(get_session)
|
||||||
path: str,
|
) -> Response | StreamingResponse:
|
||||||
api_key: APIKey = Depends(APIKeyAuth())
|
"""Forward requests to upstream provider and charge usage."""
|
||||||
) -> Response:
|
|
||||||
"""Forward requests to upstream provider"""
|
|
||||||
# Build upstream request
|
|
||||||
# Stream response
|
|
||||||
# Track usage
|
|
||||||
# Deduct costs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Key features:
|
Key features:
|
||||||
@@ -215,27 +187,29 @@ Key features:
|
|||||||
|
|
||||||
#### `routstr/core/db.py`
|
#### `routstr/core/db.py`
|
||||||
|
|
||||||
SQLModel definitions:
|
SQLModel definitions (selected):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class APIKey(SQLModel, table=True):
|
class ApiKey(SQLModel, table=True):
|
||||||
id: int | None = Field(primary_key=True)
|
hashed_key: str = Field(primary_key=True)
|
||||||
key_hash: str = Field(index=True, unique=True)
|
balance: int
|
||||||
balance: int # millisatoshis
|
reserved_balance: int = 0
|
||||||
total_deposited: int = 0
|
refund_address: str | None = None
|
||||||
|
key_expiry_time: int | None = None
|
||||||
total_spent: int = 0
|
total_spent: int = 0
|
||||||
created_at: datetime
|
total_requests: int = 0
|
||||||
expires_at: datetime | None = None
|
|
||||||
metadata: dict = Field(default_factory=dict, sa_column=Column(JSON))
|
|
||||||
|
|
||||||
class Transaction(SQLModel, table=True):
|
class LightningInvoice(SQLModel, table=True):
|
||||||
id: int | None = Field(primary_key=True)
|
id: str = Field(primary_key=True)
|
||||||
api_key_id: int = Field(foreign_key="apikey.id")
|
bolt11: str
|
||||||
amount: int # can be negative
|
amount_sats: int
|
||||||
balance_after: int
|
status: str
|
||||||
type: TransactionType
|
|
||||||
description: str
|
class UpstreamProviderRow(SQLModel, table=True):
|
||||||
timestamp: datetime
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
|
provider_type: str
|
||||||
|
base_url: str
|
||||||
|
api_key: str
|
||||||
```
|
```
|
||||||
|
|
||||||
### Admin Interface
|
### Admin Interface
|
||||||
@@ -245,18 +219,17 @@ class Transaction(SQLModel, table=True):
|
|||||||
Web dashboard and admin API:
|
Web dashboard and admin API:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@admin_router.get("/admin/")
|
@admin_router.get("/admin")
|
||||||
async def admin_dashboard(request: Request):
|
async def admin_dashboard(request: Request):
|
||||||
"""Render admin HTML interface"""
|
"""Render admin HTML interface"""
|
||||||
# Authentication check
|
# Authentication check
|
||||||
# Load statistics
|
# Load statistics
|
||||||
# Render template
|
# Render template
|
||||||
|
|
||||||
@admin_router.post("/admin/api/withdraw")
|
@admin_router.post("/admin/withdraw")
|
||||||
async def withdraw_balance(
|
async def withdraw_balance(
|
||||||
api_key: str,
|
request: Request, withdraw_request: WithdrawRequest
|
||||||
amount: int | None = None
|
) -> dict[str, str]:
|
||||||
) -> WithdrawalResponse:
|
|
||||||
"""Generate eCash token for withdrawal"""
|
"""Generate eCash token for withdrawal"""
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -271,25 +244,14 @@ Features:
|
|||||||
|
|
||||||
#### `routstr/wallet.py`
|
#### `routstr/wallet.py`
|
||||||
|
|
||||||
Cashu wallet operations:
|
Cashu wallet operations (function-based):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class WalletManager:
|
async def recieve_token(token: str) -> tuple[int, str, str]:
|
||||||
"""Manage Cashu wallet instances"""
|
"""Redeem eCash token and return amount/unit/mint."""
|
||||||
|
|
||||||
async def redeem_token(
|
async def send_token(amount: int, unit: str, mint_url: str | None = None) -> str:
|
||||||
self,
|
"""Create eCash token for withdrawal."""
|
||||||
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"""
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Utility Modules
|
### Utility Modules
|
||||||
@@ -301,12 +263,9 @@ Structured logging configuration:
|
|||||||
```python
|
```python
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
"""Configure JSON structured logging"""
|
"""Configure JSON structured logging"""
|
||||||
# Set log level
|
|
||||||
# Configure formatters
|
|
||||||
# Add handlers
|
|
||||||
|
|
||||||
class RequestIdMiddleware:
|
class RequestIdFilter(logging.Filter):
|
||||||
"""Add request ID to all logs"""
|
"""Attach request ID to log records."""
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `routstr/core/middleware.py`
|
#### `routstr/core/middleware.py`
|
||||||
@@ -316,27 +275,18 @@ HTTP middleware components:
|
|||||||
```python
|
```python
|
||||||
class LoggingMiddleware:
|
class LoggingMiddleware:
|
||||||
"""Log all HTTP requests/responses"""
|
"""Log all HTTP requests/responses"""
|
||||||
|
|
||||||
class ErrorHandlingMiddleware:
|
|
||||||
"""Consistent error responses"""
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `routstr/core/exceptions.py`
|
#### `routstr/core/exceptions.py`
|
||||||
|
|
||||||
Custom exception hierarchy:
|
Exception handlers:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class RoustrError(Exception):
|
async def http_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||||
"""Base exception with error details"""
|
"""HTTP exception handler with request ID"""
|
||||||
status_code: int
|
|
||||||
error_type: str
|
|
||||||
detail: str
|
|
||||||
|
|
||||||
class PaymentError(RoustrError):
|
async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||||
"""Payment-related errors"""
|
"""Fallback exception handler with request ID"""
|
||||||
|
|
||||||
class UpstreamError(RoustrError):
|
|
||||||
"""Upstream API errors"""
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
@@ -348,7 +298,7 @@ Project metadata and dependencies:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
name = "routstr"
|
name = "routstr"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard]>=0.115",
|
"fastapi[standard]>=0.115",
|
||||||
"sqlmodel>=0.0.24",
|
"sqlmodel>=0.0.24",
|
||||||
@@ -409,13 +359,13 @@ Using FastAPI's DI system:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# Define dependency
|
# Define dependency
|
||||||
async def get_db() -> AsyncSession:
|
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
async with async_session() as session:
|
async with AsyncSession(engine, expire_on_commit=False) as session:
|
||||||
yield session
|
yield session
|
||||||
|
|
||||||
# Use in routes
|
# Use in routes
|
||||||
@router.get("/items")
|
@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))
|
result = await db.execute(select(Item))
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
```
|
```
|
||||||
|
|||||||
+73
-38
@@ -22,22 +22,7 @@ git clone https://github.com/YOUR_USERNAME/routstr-core.git
|
|||||||
cd routstr-core
|
cd routstr-core
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Install uv
|
### 2. Set Up Environment
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Run the setup command:
|
Run the setup command:
|
||||||
|
|
||||||
@@ -47,13 +32,13 @@ make setup
|
|||||||
|
|
||||||
This will:
|
This will:
|
||||||
|
|
||||||
- ✅ Install uv if not present
|
- ✅ Install [uv](https://github.com/astral-sh/uv) if not present
|
||||||
- ✅ Create a virtual environment
|
- ✅ Create a virtual environment
|
||||||
- ✅ Install all dependencies
|
- ✅ Install all dependencies
|
||||||
- ✅ Install dev tools (mypy, ruff, pytest)
|
- ✅ Install dev tools (mypy, ruff, pytest)
|
||||||
- ✅ Install project in editable mode
|
- ✅ Install project in editable mode
|
||||||
|
|
||||||
### 4. Configure Environment
|
### 3. Configure Environment
|
||||||
|
|
||||||
Create your environment file:
|
Create your environment file:
|
||||||
|
|
||||||
@@ -71,7 +56,7 @@ ADMIN_PASSWORD=development-password
|
|||||||
DATABASE_URL=sqlite+aiosqlite:///dev.db
|
DATABASE_URL=sqlite+aiosqlite:///dev.db
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Verify Installation
|
### 4. Verify Installation
|
||||||
|
|
||||||
Run these commands to verify your setup:
|
Run these commands to verify your setup:
|
||||||
|
|
||||||
@@ -168,42 +153,92 @@ Understanding the codebase:
|
|||||||
```
|
```
|
||||||
routstr-core/
|
routstr-core/
|
||||||
├── routstr/ # Main package
|
├── routstr/ # Main package
|
||||||
│ ├── __init__.py # Package initialization
|
│ ├── __init__.py
|
||||||
|
│ ├── algorithm.py # Provider selection algorithms
|
||||||
│ ├── auth.py # Authentication logic
|
│ ├── auth.py # Authentication logic
|
||||||
│ ├── balance.py # Balance management
|
│ ├── balance.py # Balance management API
|
||||||
│ ├── discovery.py # Nostr discovery
|
│ ├── discovery.py # Nostr discovery
|
||||||
|
│ ├── lightning.py # Lightning invoice handling
|
||||||
|
│ ├── nip91.py # Node announcement implementation
|
||||||
│ ├── proxy.py # Request proxying
|
│ ├── proxy.py # Request proxying
|
||||||
│ ├── wallet.py # Cashu wallet integration
|
│ ├── wallet.py # Cashu wallet integration
|
||||||
│ │
|
│ │
|
||||||
│ ├── core/ # Core modules
|
│ ├── core/ # Core modules
|
||||||
│ │ ├── admin.py # Admin dashboard
|
│ │ ├── admin.py # Admin dashboard API
|
||||||
│ │ ├── db.py # Database models
|
│ │ ├── db.py # Database models (SQLModel)
|
||||||
│ │ ├── exceptions.py # Custom exceptions
|
│ │ ├── exceptions.py # Custom exceptions
|
||||||
|
│ │ ├── log_manager.py # Log management
|
||||||
│ │ ├── logging.py # Logging setup
|
│ │ ├── logging.py # Logging setup
|
||||||
│ │ ├── main.py # FastAPI app
|
│ │ ├── main.py # FastAPI app entry
|
||||||
│ │ └── middleware.py # HTTP middleware
|
│ │ ├── middleware.py # HTTP middleware
|
||||||
|
│ │ └── settings.py # Configuration
|
||||||
│ │
|
│ │
|
||||||
│ └── payment/ # Payment processing
|
│ ├── payment/ # Payment processing
|
||||||
│ ├── cost_calculation.py # Cost logic
|
│ │ ├── cost_calculation.py
|
||||||
│ ├── helpers.py # Utilities
|
│ │ ├── helpers.py
|
||||||
│ ├── lnurl.py # Lightning URLs
|
│ │ ├── lnurl.py # LNURL support
|
||||||
│ ├── models.py # Model pricing
|
│ │ ├── models.py # Model pricing
|
||||||
│ ├── price.py # BTC pricing
|
│ │ └── price.py # BTC/USD rates
|
||||||
│ └── x_cashu.py # Cashu headers
|
│ │
|
||||||
|
│ └── 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
|
├── tests/ # Test suite
|
||||||
│ ├── unit/ # Unit tests
|
├── migrations/ # Alembic migrations
|
||||||
│ └── integration/ # Integration tests
|
|
||||||
│
|
|
||||||
├── migrations/ # Database migrations
|
|
||||||
├── scripts/ # Utility scripts
|
├── scripts/ # Utility scripts
|
||||||
├── docs/ # Documentation
|
├── docs/ # Documentation
|
||||||
|
├── examples/ # Usage examples
|
||||||
│
|
│
|
||||||
├── Makefile # Dev commands
|
├── Makefile # Dev commands
|
||||||
├── pyproject.toml # Project config
|
├── pyproject.toml # Project config
|
||||||
└── compose.yml # Docker setup
|
└── 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
|
## Common Tasks
|
||||||
|
|
||||||
### Adding a New Endpoint
|
### Adding a New Endpoint
|
||||||
@@ -383,7 +418,7 @@ rm -rf test_*.db
|
|||||||
Now that you're set up:
|
Now that you're set up:
|
||||||
|
|
||||||
1. Read the [Architecture Overview](architecture.md)
|
1. Read the [Architecture Overview](architecture.md)
|
||||||
3. Check [open issues](https://github.com/routstr/routstr-core/issues)
|
2. Check [open issues](https://github.com/routstr/routstr-core/issues)
|
||||||
4. Start with a small contribution
|
3. Start with a small contribution
|
||||||
|
|
||||||
Happy coding! 🚀
|
Happy coding! 🚀
|
||||||
|
|||||||
+166
-514
@@ -6,576 +6,228 @@ This guide covers testing practices, patterns, and tools used in Routstr Core de
|
|||||||
|
|
||||||
We follow these principles:
|
We follow these principles:
|
||||||
|
|
||||||
- **Test Behavior, Not Implementation** - Tests should survive refactoring
|
- Test behavior, not implementation
|
||||||
- **Fast Feedback** - Unit tests run in milliseconds
|
- Fast feedback
|
||||||
- **Reliable Tests** - No flaky tests allowed
|
- Reliable tests
|
||||||
- **Clear Failures** - Tests should clearly indicate what broke
|
- Clear failures
|
||||||
|
|
||||||
## Test Structure
|
## Test Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
tests/
|
tests/
|
||||||
├── __init__.py
|
├── integration/
|
||||||
├── conftest.py # Shared fixtures and configuration
|
│ ├── conftest.py
|
||||||
├── unit/ # Fast, isolated unit tests
|
│ ├── utils.py
|
||||||
│ ├── test_auth.py
|
│ ├── test_wallet_topup.py
|
||||||
│ ├── test_balance.py
|
│ ├── test_wallet_refund.py
|
||||||
│ ├── test_cost_calculation.py
|
│ ├── test_wallet_information.py
|
||||||
│ ├── test_models.py
|
│ ├── test_proxy_get_endpoints.py
|
||||||
│ └── test_wallet.py
|
│ ├── test_proxy_post_endpoints.py
|
||||||
└── integration/ # Component integration tests
|
│ └── ... more integration tests
|
||||||
├── test_api_endpoints.py
|
├── unit/
|
||||||
├── test_payment_flow.py
|
│ ├── test_algorithm.py
|
||||||
├── test_proxy_streaming.py
|
│ ├── test_fee_consistency.py
|
||||||
└── test_real_mint.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
|
## 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
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
make test
|
pytest
|
||||||
|
|
||||||
# Run unit tests only (fast)
|
# Run a specific test file
|
||||||
make test-unit
|
pytest tests/unit/test_wallet.py -v
|
||||||
|
|
||||||
# Run integration tests
|
# Run a specific test
|
||||||
make test-integration
|
pytest tests/unit/test_wallet.py::test_get_balance -v
|
||||||
|
|
||||||
# Run with coverage
|
# Run tests matching a pattern
|
||||||
make test-coverage
|
pytest -k "wallet" -v
|
||||||
|
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test Markers
|
## Test Modes (Integration)
|
||||||
|
|
||||||
Use markers to categorize tests:
|
Integration tests support two execution modes:
|
||||||
|
|
||||||
```python
|
- Mock mode (default): uses in-memory mocks, no Docker required
|
||||||
@pytest.mark.slow
|
- Docker mode: uses real Docker services (Cashu mint, mock OpenAI, Nostr relay)
|
||||||
async def test_heavy_computation():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@pytest.mark.requires_docker
|
Use the runner script for Docker mode:
|
||||||
async def test_real_services():
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Run without slow tests
|
```bash
|
||||||
pytest -m "not slow"
|
./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
|
# 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
|
## Writing Tests
|
||||||
|
|
||||||
### Unit Test Example
|
### Unit Test Example
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# tests/unit/test_auth.py
|
from routstr.algorithm import calculate_model_cost_score
|
||||||
import pytest
|
from routstr.payment.models import Architecture, Model, Pricing
|
||||||
from routstr.auth import create_api_key, validate_api_key
|
|
||||||
|
|
||||||
class TestAPIKeyAuth:
|
|
||||||
"""Test API key authentication functionality"""
|
|
||||||
|
|
||||||
async def test_create_api_key(self, test_db):
|
def test_calculate_model_cost_score_basic() -> None:
|
||||||
"""Test creating a new API key"""
|
model = Model(
|
||||||
# Arrange
|
id="test-model",
|
||||||
initial_balance = 10000
|
name="Test test-model",
|
||||||
|
created=1234567890,
|
||||||
# Act
|
description="Test model",
|
||||||
api_key = await create_api_key(
|
context_length=8192,
|
||||||
balance=initial_balance,
|
architecture=Architecture(
|
||||||
name="Test Key"
|
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
|
||||||
# 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")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Integration Test Example
|
### Integration Test Example
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# tests/integration/test_payment_flow.py
|
|
||||||
import pytest
|
import pytest
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
class TestPaymentFlow:
|
|
||||||
"""Test end-to-end payment flows"""
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_token_redemption_flow(
|
async def test_wallet_topup(
|
||||||
self,
|
authenticated_client: AsyncClient,
|
||||||
app_client: AsyncClient,
|
testmint_wallet: object,
|
||||||
mock_cashu_wallet
|
db_snapshot: object,
|
||||||
):
|
) -> None:
|
||||||
"""Test complete token redemption and API usage"""
|
await db_snapshot.capture()
|
||||||
# Arrange
|
token = await testmint_wallet.mint_tokens(1000)
|
||||||
token = create_test_token(amount=5000)
|
response = await authenticated_client.post(
|
||||||
mock_cashu_wallet.redeem.return_value = 5000
|
"/v1/wallet/topup", params={"cashu_token": token}
|
||||||
|
|
||||||
# Act - Create API key
|
|
||||||
response = await app_client.post(
|
|
||||||
"/v1/wallet/create",
|
|
||||||
json={"cashu_token": token}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Assert - Key created
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
api_key = response.json()["api_key"]
|
diff = await db_snapshot.diff()
|
||||||
assert response.json()["balance"] == 5000
|
assert len(diff["api_keys"]["modified"]) == 1
|
||||||
|
|
||||||
# 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
|
## Debugging Tips
|
||||||
|
|
||||||
### 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.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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate coverage report
|
# Show print output
|
||||||
make test-coverage
|
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
|
# Drop into debugger on failure
|
||||||
pytest --pdb
|
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
|
## Troubleshooting
|
||||||
|
|
||||||
### Common Issues
|
- 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
|
||||||
**Async Test Errors**
|
- Slow tests: use `pytest -m "not slow"` or `make test-fast`
|
||||||
|
|
||||||
```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"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- See [Architecture](architecture.md) for system design
|
- See [Architecture](architecture.md)
|
||||||
- Read [Setup Guide](setup.md) for environment setup
|
- Read [Setup Guide](setup.md)
|
||||||
|
|||||||
+28
-67
@@ -1,83 +1,44 @@
|
|||||||
# Routstr Core Documentation
|
# 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
|
If you want to use AI models in your application without accounts or KYC.
|
||||||
- **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
|
|
||||||
|
|
||||||
### 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
|
## 🦁 For Providers (Node Operators)
|
||||||
- 🔑 **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
|
|
||||||
|
|
||||||
## How It Works
|
If you want to run a node, resell API access, or monetize hardware.
|
||||||
|
|
||||||
```mermaid
|
- **[Quick Start](provider/quickstart.md)**: Deploy a node in 5 minutes.
|
||||||
sequenceDiagram
|
- **[Deployment](provider/deployment.md)**: Production Docker setup.
|
||||||
participant Client
|
- **[Configuration](provider/configuration.md)**: Environment variables and settings.
|
||||||
participant Routstr as Routstr Proxy
|
- **[Dashboard](provider/dashboard.md)**: Managing your node visually.
|
||||||
participant DB as Database
|
- **[Pricing Strategy](provider/pricing.md)**: Setting margins and fees.
|
||||||
participant Upstream as AI Provider
|
- **[Discovery](provider/discovery.md)**: Announcing your node on Nostr.
|
||||||
participant Wallet as Cashu Wallet
|
- **[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
|
*Powered by [Cashu](https://cashu.space) and [Nostr](https://nostr.com).*
|
||||||
|
|
||||||
- :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.
|
|
||||||
|
|||||||
@@ -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) |
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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 -->
|
||||||
@@ -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 .
|
||||||
|
```
|
||||||
@@ -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()}")
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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")
|
||||||
|
)
|
||||||
|
```
|
||||||
+19
-21
@@ -77,29 +77,27 @@ extra:
|
|||||||
|
|
||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
- Getting Started:
|
- Overview: overview.md
|
||||||
- Overview: getting-started/overview.md
|
- Client Guide:
|
||||||
- Quick Start: getting-started/quickstart.md
|
- Introduction: client/introduction.md
|
||||||
- Docker Setup: getting-started/docker.md
|
- Payment Flow: client/payments.md
|
||||||
- Configuration: getting-started/configuration.md
|
- Integration: client/integration.md
|
||||||
- User Guide:
|
- Provider Guide:
|
||||||
- Introduction: user-guide/introduction.md
|
- Quick Start: provider/quickstart.md
|
||||||
- Payment Flow: user-guide/payment-flow.md
|
- Dashboard: provider/dashboard.md
|
||||||
- Using the API: user-guide/using-api.md
|
- Deployment: provider/deployment.md
|
||||||
- Admin Dashboard: user-guide/admin-dashboard.md
|
- Configuration: provider/configuration.md
|
||||||
- Models & Pricing: user-guide/models-pricing.md
|
- Pricing: provider/pricing.md
|
||||||
- Contributing:
|
- Advanced Pricing: provider/advanced-pricing.md
|
||||||
- Setup Development: contributing/setup.md
|
- Discovery: provider/discovery.md
|
||||||
- Architecture: contributing/architecture.md
|
- Tor Support: provider/tor.md
|
||||||
- Code Structure: contributing/code-structure.md
|
|
||||||
- Testing: contributing/testing.md
|
|
||||||
- API Reference:
|
- API Reference:
|
||||||
- Overview: api/overview.md
|
- Overview: api/overview.md
|
||||||
- Authentication: api/authentication.md
|
- Authentication: api/authentication.md
|
||||||
- Endpoints: api/endpoints.md
|
- Endpoints: api/endpoints.md
|
||||||
- Errors: api/errors.md
|
- Errors: api/errors.md
|
||||||
- Advanced:
|
- Contributing:
|
||||||
- Tor Support: advanced/tor.md
|
- Setup Development: contributing/setup.md
|
||||||
- Nostr Discovery: advanced/nostr.md
|
- Architecture: contributing/architecture.md
|
||||||
- Custom Pricing: advanced/custom-pricing.md
|
- Code Structure: contributing/code-structure.md
|
||||||
- Migrations: advanced/migrations.md
|
- Testing: contributing/testing.md
|
||||||
|
|||||||
Reference in New Issue
Block a user