Agent Margin Router
18 x402 pay-per-call tools for AI agents (USDC/USDT, Base): web, crypto, DeFi, FX, geo-IP, RSS, ENS
Open source Repository Open in the app JSON README (API)
About
18 x402 pay-per-call tools for AI agents (USDC/USDT, Base): web, crypto, DeFi, FX, geo-IP, RSS, ENS
Details
- Kind
- MCP servers
- Topic
- Finance & crypto
- Publisher
- agentmarginrouter
- Origin
- official
- Category
- ferramentas
- Transport
- http
- Version
- 0.4.0
- Last push
- 2026-09-11T22:22:08Z
- Repository state
- ativo
- Language
- Python
- Added
- 2026-09-11 16:02:06
- Updated
- 2026-09-11 22:03:17
- Origin id
io.github.AgentMarginRouter/agent-margin-router
README
# Agent Margin Router
[](https://agent-margin-router-production.up.railway.app/health)
[](https://agentmarginrouter.com)
[](https://www.x402.org)
[](#)
[](#9-tests--projektstruktur)
> **EN — TL;DR:** Pay-per-request data broker for AI agents. One API, 7 upstream providers, automatic routing by quality × cost × latency, paid with **x402 micropayments (USDC or USDT on Base)** — no account, no API key, no subscription. 3 free requests per wallet.
>
> - **Live API:** `https://agent-margin-router-production.up.railway.app`
> - **Docs & examples:** https://agentmarginrouter.com
> - **Endpoints:** `POST /extract-clean` (URL → clean JSON, $0.02) · `POST /market-spread` (net spread between two venues after fees, $0.05) · `GET /health` (free)
>
> ```bash
> # Try it free (3 requests per wallet):
> curl -X POST https://agent-margin-router-production.up.railway.app/market-spread \
> -H "Content-Type: application/json" -H "X-WALLET: 0xYourWallet" \
> -d '{"asset":"ethereum","buy_venue":"binance","sell_venue":"coinbase"}'
> ```
>
> Full German documentation below · Vollständige deutsche Dokumentation folgt.
> Einheitliche, per **x402** bezahlte Routing-Schicht für AI-Agenten.
> Mehrere Daten-Provider → automatische Auswahl (Qualität, Preis, Latenz, Ausfallrisiko) → normalisiertes JSON → Bezahlung pro Request in **USDC oder USDT auf Base**.
```text
Agent ──POST /extract-clean──▶ Router ──▶ Provider-Scoring ──▶ bester Provider
▲ │ (Fallback, Circuit-Breaker, Mindestmarge)
│ 402 Payment Required │
│ (USDC + USDT Optionen) ▼
└──X-PAYMENT (signiert)──▶ Facilitator /verify ──▶ 200 OK + Daten ──▶ /settle on-chain
```
## Inhalt
1. [Was ist der Agent Margin Router](#1-was-ist-der-agent-margin-router)
2. [Schnellstart (lokal ohne Docker)](#2-schnellstart-lokal-ohne-docker)
3. [Deployment mit Docker-Compose auf Hetzner](#3-deployment-mit-docker-compose-auf-hetzner)
4. [Cloudflare Tunnel Setup](#4-cloudflare-tunnel-setup)
4b. [Deployment auf Railway](#4b-deployment-auf-railway)
5. [Umgebungsvariablen](#5-umgebungsvariablen)
6. [API-Endpunkte mit Beispielen](#6-api-endpunkte)
6b. [Enterprise: API-Keys, Rate-Limiting, Usage & Stats](#6b-enterprise-api-keys-rate-limiting-usage--stats)
7. [x402 Payment Flow](#7-x402-payment-flow)
8. [Provider hinzufügen](#8-provider-hinzufügen)
9. [Tests & Projektstruktur](#9-tests--projektstruktur)
---
## 1. Was ist der Agent Margin Router
Der Router ist ein **B2B-Daten-Broker für Maschinen**. Ein AI-Agent stellt eine standardisierte Anfrage, der Router
1. prüft den Cache (Redis),
2. bewertet alle passenden Provider mit `score = quality × success_probability / (price + latency_risk)`,
3. führt den Request nur aus, wenn die **Mindestmarge** (Default 20 %) erhalten bleibt,
4. fällt bei Fehlern automatisch auf den nächsten Provider zurück (**Circuit-Breaker**: 3 Fehler → 5 Minuten Pause),
5. normalisiert die Antwort und liefert sie mit Aktualitäts- und Konfidenzangabe aus,
6. protokolliert Kosten, Provider und Marge als strukturierte JSON-Logs.
Bezahlt wird **pro Request** über das x402-Protokoll (HTTP 402). Akzeptiert werden von Anfang an **USDC und USDT auf Base**. Neue Wallets erhalten 3 kostenlose Requests (Free Tier, in Redis getrackt).
**MVP-Endpunkte:**
| Endpunkt | Funktion | Preis (Default) |
|---|---|---|
| `POST /extract-clean` | URL → sauberes, schema-konformes JSON | 0,02 USD |
| `POST /market-spread` | Netto-Spread zwischen zwei Handelsplätzen nach Gebühren, Slippage & Transfer | 0,05 USD |
| `GET /health` | Status, Redis, Provider, Uptime, Fehlerrate, Cache-Hit-Rate | kostenlos |
> **Provider (echte APIs):**
>
> | Name | Capability | Anbieter | Kosten/Call (Default) | Qualität |
> |---|---|---|---|---|
> | `apify_web_scraper` | extract | Apify Actor `apify/web-scraper` (Headless-Browser, optional Proxy) | ~0,005 USD (wird aus `usageTotalUsd` nachjustiert) | 0.90 |
> | `httpx_direct` | extract | direkter HTTP-Abruf, kein JS | ~0,0002 USD | 0.60 |
> | `binance_public` | market_data | Binance Public API `/ticker/24hr` (Bid/Ask) via `data-api.binance.vision` – `api.binance.com` ist regional geo-blockiert | 0 USD, kein Key | 0.95 |
> | `coinbase_public` | market_data | Coinbase Public API `/prices/{pair}/spot` | 0 USD, kein Key | 0.90 |
> | `coingecko` | market_data | CoinGecko `/simple/price`, `/coins/{id}/tickers`, optional `/market_chart` | 0 USD (Demo-Key, Rate-Limit) | 0.88 |
> | `defillama` | market_data | DeFi Llama `coins.llama.fi/prices/current/coingecko:{id}` (Referenzpreis) + `api.llama.fi/tvl/{slug}` (DEX-Tiefe) | 0 USD, kein Key | 0.75 |
> | `coinmarketcap` | market_data | CMC `/cryptocurrency/quotes/latest` (Fallback, nur Referenzpreis) | ~0,0004 USD (Credits) | 0.70 |
>
> Ohne gesetzten API-Key wird ein Provider **nicht geroutet** (`is_configured == False`), taucht aber in `/health` mit `configured: false` auf.
> Der **Free-Tier** nutzt nur Provider mit `cost_per_request <= FREE_TIER_MAX_PROVIDER_COST_USD` (Default 0,001 USD) – also `httpx_direct` bzw. `binance_public`/`coinbase_public`/`coingecko`/`defillama`; `/market-spread` holt dabei jedes Leg vom **eigenen Exchange-Feed** (Binance Bid/Ask, Coinbase Spot), wenn Binance oder Coinbase Teil des Venue-Paars ist; teure Provider sind für kostenlose Requests gesperrt.
> `/extract-clean` prüft Ziel-URLs gegen private/interne Netze (SSRF-Guard: nur http/https, keine RFC-1918-, Loopback-, Link-Local- oder Metadata-Adressen, Prüfung pro Redirect-Hop); abgelehnte URLs liefern `422` und zählen nicht als Provider-Fehler.
>
> **Einschränkung `/market-spread`:** Das Ergebnis ist eine **Analyse**, keine ausführbare Quote. Preise sind Last-Trade-Werte der Venue-Ticker (CoinGecko) bzw. ein Referenzpreis (CMC); „Liquidität“ wird aus 24h-Volumen und einem statischen Depth-Anteil je Venue geschätzt, nicht aus dem Orderbuch. Das Feld `data_source`/`venue_data_source` (`ticker`, `mixed`, `reference`) und `note` im Response kennzeichnen die Datenbasis.
---
## 2. Schnellstart (lokal ohne Docker)
Voraussetzungen: Python ≥ 3.11, optional ein lokaler Redis (ohne Redis nutzt die App im Dev-Modus automatisch einen In-Memory-Fallback).
```bash
git clone <dein-repo> agent_margin_router
cd agent_margin_router
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
cp .env.example .env
# Für lokale Tests ohne Wallet/Zahlung:
sed -i 's/^PAYMENT_ENABLED=.*/PAYMENT_ENABLED=false/' .env
sed -i 's#^REDIS_URL=.*#REDIS_URL=redis://localhost:6379/0#' .env
uvicorn app.main:app --reload --port 8000
```
Dann:
```bash
curl -s localhost:8000/health | jq .status
curl -s -X POST localhost:8000/extract-clean \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/produkt/123"}' | jq
```
Interaktive API-Doku: <http://localhost:8000/docs>
Tests:
```bash
pytest -q
```
---
## 3. Deployment mit Docker-Compose auf Hetzner
Getestet für einen Hetzner Cloud Server (CX22 reicht für den Start, Ubuntu 24.04).
### 3.1 Server vorbereiten
```bash
ssh root@<SERVER-IP>
# System aktualisieren, Docker installieren
apt update && apt upgrade -y
curl -fsSL https://get.docker.com | sh
# Nicht-Root-User für den Betrieb
adduser --disabled-password --gecos "" deploy
usermod -aG docker deploy
# Firewall: nur SSH offen lassen (HTTP kommt über Cloudflare Tunnel, s. Abschnitt 4)
apt install -y ufw
ufw allow OpenSSH
ufw --force enable
```
### 3.2 Projekt deployen
```bash
su - deploy
git clone <dein-repo> agent_margin_router
cd agent_margin_router
cp .env.example .env
nano .env # RECEIVER_WALLET, ENVIRONMENT=production, REDIS_REQUIRED=true, Preise, Provider-Keys
```
Wichtige Produktionswerte in `.env`:
```dotenv
ENVIRONMENT=production
REDIS_URL=redis://redis:6379/0
REDIS_REQUIRED=true
PAYMENT_ENABLED=true
RECEIVER_WALLET=0xDeineEchteWallet
```
Starten:
```bash
docker compose up -d --build
docker compose ps
docker compose logs -f app
curl -s localhost:8000/health | jq
```
Der App-Port `8000` ist im Compose-File **nur auf 127.0.0.1** gebunden – von außen ist der Dienst ausschließlich über den Cloudflare Tunnel (oder einen eigenen Reverse Proxy) erreichbar.
### 3.3 Betrieb
```bash
docker compose pull && docker compose up -d --build # Update
docker compose logs --tail=200 app # JSON-Logs
docker compose exec redis redis-cli INFO memory # Redis prüfen
docker compose down # Stoppen (Redis-Daten bleiben im Volume)
```
Beide Services haben Healthchecks und `restart: unless-stopped`; nach einem Server-Reboot startet der Stack automatisch.
---
## 4. Cloudflare Tunnel Setup
Mit einem Cloudflare Tunnel braucht der Hetzner-Server **keinen offenen HTTP-Port**; TLS, DDoS-Schutz und WAF übernimmt Cloudflare.
1. Domain bei Cloudflare verwalten (Nameserver umstellen).
2. Cloudflare Dashboard → **Zero Trust** → **Networks** → **Tunnels** → **Create a tunnel** (Typ „Cloudflared“).
3. Namen vergeben (z. B. `agent-margin-router`) und den angezeigten **Token** kopieren.
4. Im Tunnel unter **Public Hostname** einen Eintrag anlegen:
- Subdomain: `api`, Domain: `deine-domain.tld`
- Service: `HTTP` → `app:8000` (Docker-Servicename, da cloudflared im selben Compose-Netz läuft)
5. Token in `.env` eintragen:
```dotenv
CLOUDFLARE_TUNNEL_TOKEN=eyJhIjoi...
```
6. Stack mit Tunnel-Profil starten:
```bash
docker compose --profile tunnel up -d
docker compose logs -f cloudflared # "Registered tunnel connection" = erfolgreich
curl -s https://api.deine-domain.tld/health | jq .status
```
Empfohlen zusätzlich im Cloudflare-Dashboard: **Rate Limiting Rule** auf `/extract-clean` und `/market-spread` (z. B. 60 Requests/Minute pro IP) sowie **Bot Fight Mode** deaktivieren, damit Agenten nicht blockiert werden.
---
## 4b. Deployment auf Railway
Alternative zu Hetzner/Docker-Compose. Das Repo enthält `railway.toml` (Dockerfile-Build, Healthcheck `/health`, Restart-Policy) und ein `Procfile`.
1. Neues Railway-Projekt → **Deploy from GitHub repo** (oder `railway up` per CLI).
2. **Redis-Service** hinzufügen (Datenbanken → Redis) und im App-Service die Variable `REDIS_URL=${{Redis.REDIS_URL}}` setzen; `REDIS_REQUIRED=true`.
3. Variablen aus `.env.example` im Railway-Dashboard eintragen (`RECEIVER_WALLET`, `APIFY_API_TOKEN`, `COINGECKO_API_KEY`, `CMC_API_KEY`, `ENVIRONMENT=production`, ggf. `ENABLED_TOKENS=USDC`). `PORT` setzt Railway selbst – **nicht** überschreiben.
4. Deploy abwarten, dann `curl https://<railway-domain>/health` – im Feld `providers` müssen die konfigurierten Provider `configured: true` melden.
Hinweis: Der Dockerfile-Start (`uvicorn ... --port ${PORT:-8000}`) läuft unverändert auch lokal und in docker-compose (dort bleibt Port 8000).
---
## 5. Umgebungsvariablen
Alle Variablen sind in [`.env.example`](.env.example) mit Kommentaren dokumentiert. Die wichtigsten:
| Variable | Bedeutung | Default |
|---|---|---|
| `REDIS_URL` | Redis-Verbindung (Cache, Free-Tier-Zähler, Replay-Schutz) | `redis://redis:6379/0` |
| `REDIS_REQUIRED` | `true`: kein Start ohne Redis; `false`: In-Memory-Fallback (nur Dev) | `false` |
| `PAYMENT_ENABLED` | `false` schaltet x402 komplett ab (nur lokal!) | `true` |
| `FACILITATOR_URL` | x402-Facilitator `/verify`-Endpunkt (PayAI) | facilitator.payai.network |
| `USDC_CONTRACT_ADDRESS` | USDC auf Base | `0x8335…2913` |
| `USDT_CONTRACT_ADDRESS` | USDT auf Base | `0xfde4…9bb2` |
| `PAYMENT_NETWORK` | x402-Netzwerkname (`base`, `base-sepolia`) | `base` |
| `RECEIVER_WALLET` | **Deine** Einnahmen-Wallet | – |
| `EXTRACT_PRICE` / `MARKET_SPREAD_PRICE` | Verkaufspreise in USD | `0.02` / `0.05` |
| `FREE_TIER_LIMIT` | Kostenlose Requests pro Wallet | `3` |
| `MIN_MARGIN_PERCENT` | Mindestmarge; darunter wird nicht ausgeführt | `20` |
| `DAILY_PROVIDER_BUDGET_USD` | Tages-Kostendeckel pro Provider | `50` |
| `CACHE_TTL_SECONDS` | Standard-Cache-Dauer | `60` |
| `CIRCUIT_FAILURE_THRESHOLD` / `CIRCUIT_OPEN_SECONDS` | Circuit-Breaker | `3` / `300` |
| `ENABLED_TOKENS` | Akzeptierte Zahl-Token, kommagetrennt (`USDC` bis USDT real E2E getestet ist) | `USDC,USDT` |
| `APIFY_API_TOKEN` | Apify-Token für `apify_web_scraper` (leer = Provider aus) | leer |
| `APIFY_ACTOR_ID` / `APIFY_TIMEOUT_SECONDS` / `APIFY_USE_PROXY` | Actor, Run-Timeout, Apify-Proxy | `apify~web-scraper` / `90` / `true` |
| `APIFY_COST_PER_REQUEST` | Startannahme Kosten/Call (wird aus Run-Usage nachgeführt) | `0.005` |
| `COINGECKO_API_KEY` | CoinGecko Demo/Pro-Key für `coingecko` | leer |
| `COINGECKO_USE_MARKET_CHART` | zusätzlich 24h-Volatilität abrufen (1 Call mehr) | `true` |
| `CMC_API_KEY` | CoinMarketCap-Key für Fallback `coinmarketcap` | leer |
| `HTTPX_FALLBACK_ENABLED` | günstigen `httpx_direct`-Fallback registrieren | `true` |
| `FREE_TIER_MAX_PROVIDER_COST_USD` | Kostendeckel je Call für Free-Tier-Requests | `0.001` |
| `PAID_MIN_QUALITY` | bezahlte Requests bevorzugen Provider ab dieser Qualität | `0.8` |
| `API_KEY_RATE_LIMIT_PER_MINUTE` / `API_KEY_RATE_LIMIT_MAX` | Standard-Limit pro API-Key je Fenster / harter Deckel | `100` / `1000` |
| `ANON_RATE_LIMIT_PER_MINUTE` | Limit pro IP ohne API-Key und ohne `X-WALLET` | `10` |
| `RATE_LIMIT_WINDOW_SECONDS` | Länge des Sliding Windows | `60` |
| `API_KEY_ADMIN_SECRET` | Wenn gesetzt: `X-ADMIN-SECRET` nötig für Key-Erzeugung/-Deaktivierung (**in Produktion setzen**) | leer |
| `USAGE_RECENT_LIMIT` / `USAGE_DAILY_TTL_SECONDS` | Letzte Requests pro Identität / Aufbewahrung der Tageswerte | `20` / `172800` |
| `CLOUDFLARE_TUNNEL_TOKEN` | Nur für `--profile tunnel` | leer |
Für **Testnet-Betrieb** (empfohlen vor Mainnet): `PAYMENT_NETWORK=base-sepolia`, `PAYMENT_NETWORK_CHAIN_ID=84532`, `FACILITATOR_URL=https://x402.org/facilitator/verify` und die Sepolia-Token-Adressen eintragen.
---
## 6. API-Endpunkte
### `GET /health` (kostenlos)
```bash
curl -s https://api.deine-domain.tld/health | jq
```
```json
{
"status": "ok",
"version": "0.1.0",
"environment": "production",
"timestamp": "2026-09-10T12:00:00Z",
"uptime_seconds": 86400.2,
"redis_connected": true,
"redis_backend": "redis",
"request_count": 1520,
"error_count": 3,
"error_rate": 0.002,
"cache_hits": 610,
"cache_misses": 900,
"cache_hit_rate": 0.404,
"providers": [
{"name": "apify_web_scraper", "capability": "extract", "healthy": true, "circuit_open": false,
"consecutive_failures": 0, "success_rate": 0.98, "avg_latency_ms": 61.2, "requests": 700}
]
}
```
`status` ist `ok`, `degraded` (z. B. eine Capability ohne gesunden Provider oder Redis im Memory-Fallback) oder `down` (HTTP 503, Redis fehlt und ist Pflicht).
### `POST /extract-clean` (0,02 USD)
Request:
```bash
curl -s -X POST https://api.deine-domain.tld/extract-clean \
-H 'Content-Type: application/json' \
-H 'X-WALLET: 0xDeineWallet' \
-d '{
"url": "https://shop.example.com/produkt/123",
"schema": {"type":"object","properties":{"title":{},"price":{},"currency":{},"availability":{}}},
"max_age_seconds": 60
}' | jq
```
Response:
```json
{
"data": {"title": "Shop – Product Overview", "price": 149.9, "currency": "EUR", "availability": "in_stock"},
"sources_checked": 1,
"freshness_seconds": 0,
"confidence": 0.91,
"cost_usdc": 0.02,
"token": "USDC",
"fetched_at": "2026-09-10T12:00:01Z",
"routing": {"provider": "apify_web_scraper", "fallback_used": false,
"providers_tried": ["apify_web_scraper"], "cache_hit": false, "latency_ms": 74}
}
```
Felder: `schema` (optional) formt die Ausgabe auf die Top-Level-Properties; `max_age_seconds: 0` erzwingt einen frischen Abruf.
### `POST /market-spread` (0,05 USD)
```bash
curl -s -X POST https://api.deine-domain.tld/market-spread \
-H 'Content-Type: application/json' \
-H 'X-WALLET: 0xDeineWallet' \
-d '{"asset":"ETH","buy_venue":"binance","sell_venue":"coinbase","size_usd":5000}' | jq
```
```json
{
"asset": "ETH", "quote": "USDT", "buy_venue": "binance", "sell_venue": "coinbase",
"buy_price": 4247.31, "sell_price": 4258.02,
"gross_spread_bps": 25.2, "fees_bps": 70.0, "slippage_bps": 3.4,
"transfer_cost": 1.31, "transfer_cost_bps": 2.62,
"net_spread_bps": -50.8, "estimated_net_profit_usd": -25.4,
"confidence": 0.86, "execution_risk": "high",
"freshness_seconds": 4, "cost_usdc": 0.05, "token": "USDT",
"fetched_at": "2026-09-10T12:00:02Z",
"routing": {"provider": "coingecko", "fallback_used": false, "providers_tried": ["coingecko"], "cache_hit": false, "latency_ms": 58}
}
```
`net_spread_bps = gross − fees − slippage − transfer_cost_bps`. `execution_risk` berücksichtigt Nettospread, Slippage und Transferzeit. Der Dienst verkauft die **Prüfung**, nicht den Trade – es gibt keine risikofreie Arbitrage.
### Fehlerformat
```json
{"error": "http_error", "detail": "No extraction provider available: ..."}
```
| Status | Bedeutung |
|---|---|
| `402` | Zahlung fehlt/ungültig oder Free Tier erschöpft (Body enthält Zahlungsoptionen) |
| `422` | Validierungsfehler im Request |
| `502` | Facilitator nicht erreichbar / Provider lieferte unbrauchbare Daten |
| `401` | `X-API-KEY` unbekannt oder deaktiviert (`{"error": "invalid_api_key"}`) |
| `429` | Rate-Limit überschritten (`{"error": "rate_limit_exceeded", "retry_after": …}`, Header `Retry-After`) |
| `503` | Kein Provider verfügbar oder Mindestmarge verletzt |
---
## 6b. Enterprise: API-Keys, Rate-Limiting, Usage & Stats
Alle Endpunkte in diesem Abschnitt sind **kostenlos** (kein x402). Persistenz komplett in Redis; ohne Redis
(`REDIS_REQUIRED=false`) greift derselbe In-Memory-Fallback wie beim Cache.
### Authentifizierung per API-Key
Ein API-Key (UUID4) ist fest an eine Wallet gebunden. Wird der Header `X-API-KEY` gesendet, ermittelt der Router
die Wallet automatisch – `X-WALLET` ist dann nicht mehr nötig. Free Tier und x402-Zahlung gelten unverändert
(der Key ersetzt nur die Identifikation und bringt ein eigenes Rate-Limit mit).
```bash
# Key erzeugen (Label und Limit optional; Limit wird auf API_KEY_RATE_LIMIT_MAX gedeckelt)
curl -s -X POST https://<host>/api-keys/generate \
-H 'Content-Type: application/json' \
-d '{"wallet": "0xabc…", "label": "trading-bot-1", "rate_limit_per_minute": 300}'
# → 201 {"api_key": "6f1c…", "wallet": "0xabc…", "label": "trading-bot-1", "rate_limit_per_minute": 300, "active": true, …}
# Verwenden
curl -s -X POST https://<host>/extract-clean -H 'X-API-KEY: 6f1c…' \
-H 'Content-Type: application/json' -d '{"url": "https://example.com"}'
# Key ansehen / deaktivieren (Deaktivierung ist idempotent; Usage-Historie bleibt erhalten)
curl -s https://<host>/api-keys/6f1c…
curl -s -X DELETE https://<host>/api-keys/6f1c…
```
Ist `API_KEY_ADMIN_SECRET` gesetzt, verlangen `POST /api-keys/generate` und `DELETE /api-keys/{key}` zusätzlich
den Header `X-ADMIN-SECRET: <secret>` (sonst `403`). Ohne Secret kann jeder Keys anlegen – nur für Entwicklung.
### Rate-Limiting (Sliding Window, Redis ZSET)
| Aufrufer | Identifikation | Limit | Quelle |
|---|---|---|---|
| API-Key | `X-API-KEY` | `rate_limit_per_minute` des Keys (Default `API_KEY_RATE_LIMIT_PER_MINUTE` = 100) | pro Key |
| Wallet | `X-WALLET` | Free Tier: `FREE_TIER_LIMIT` (3) Requests pro Wallet, danach x402 | pro Wallet |
| Anonym | keins | `ANON_RATE_LIMIT_PER_MINUTE` (10) pro Client-IP | pro IP |
Jede Antwort eines bezahlten Endpunkts (`/extract-clean`, `/market-spread`) – auch `402`/`429` – trägt:
```
X-RateLimit-Limit: 100 # Limit des Fensters (bei X-WALLET: Free-Tier-Limit)
X-RateLimit-Remaining: 97 # verbleibende Requests im aktuellen Fenster
X-RateLimit-Reset: 1789000000 # Unix-Timestamp, ab dem wieder Kontingent frei wird
Retry-After: 42 # nur bei 429
```
Das Fenster ist ein echtes Sliding Window (`RATE_LIMIT_WINDOW_SECONDS`, Default 60 s): jeder Request wird mit
Zeitstempel in ein Redis-ZSET geschrieben, ältere Einträge fallen heraus. Bei Redis-Fehlern öffnet der Limiter
(fail-open) und loggt eine Warnung – zahlende Kunden werden nie wegen eines Cache-Problems blockiert.
Ein `429` verbraucht **kein** Free-Tier-Kontingent.
### Usage-Tracking
Jeder erfolgreiche Request auf einem bezahlten Endpunkt wird pro Identität (`wallet:0x…` bzw. `key:<uuid>`)
und global gezählt: Requests (heute/gesamt, bezahlt/frei, Cache-Hits), gezahlte Kosten, Provider-Kosten,
Provider- und Endpunkt-Verteilung sowie die letzten `USAGE_RECENT_LIMIT` Requests.
```bash
curl -s 'https://<host>/usage?wallet=0xabc…' # oder ?api_key=6f1c…
```
```json
{
"identity_type": "api_key", "wallet": "0xabc…", "api_key": "6f1c…", "label": "trading-bot-1", "api_key_active": true,
"requests_today": 12, "requests_total": 340, "paid_requests_total": 337, "free_requests_total": 3, "cache_hits_total": 41,
"cost_usd_today": 0.24, "cost_usd_total": 6.74,
"providers": {"coingecko": 300, "coinmarketcap": 40}, "endpoints": {"/market-spread": 340},
"free_tier": {"limit": 3, "used": 3, "remaining": 0},
"rate_limit": {"limit": 300, "remaining": 298, "window_seconds": 60, "reset": 1789000060},
"last_request_at": "2026-09-10T10:00:00+00:00",
"last_requests": [{"at": "…", "path": "/market-spread", "provider": "coingecko", "mode": "paid", "token": "USDC",
"price_usd": 0.05, "provider_cost_usd": 0.0, "cache_hit": false, "latency_ms": 412, "status": 200}]
}
```
Hinweise: Usage per API-Key und per nackter Wallet werden getrennt geführt (der Key ist die Identität), der
**Free Tier ist jedoch wallet-gebunden** und damit geteilt. Anonyme x402-Zahlungen werden der Payer-Wallet
aus der Facilitator-Verifikation zugeordnet.
### Globale Statistiken
```bash
curl -s https://<host>/stats
```
```json
{
"requests_total": 10234, "requests_today": 311, "paid_requests_total": 9800, "free_requests_total": 434,
"cache_hits_total": 1200, "active_wallets_total": 87, "active_wallets_today": 14, "active_api_keys": 23,
"revenue_usd_total": 312.55, "revenue_usd_today": 9.10, "provider_cost_usd_total": 41.02,
"gross_margin_usd_total": 271.53, "providers": {"coingecko": 7000, "httpx_direct": 2100, "apify_web_scraper": 1134},
"endpoints": {"/market-spread": 7000, "/extract-clean": 3234}, "last_request_at": "…", "generated_at": "…"
}
```
`revenue_usd_*` summiert nur tatsächlich per x402 bezahlte Requests; `active_wallets_*` zählt eindeutige Wallets
(API-Key-Wallets, `X-WALLET`-Wallets und Payer-Wallets).
---
## 7. x402 Payment Flow
```text
1) Agent → POST /extract-clean (ohne X-PAYMENT)
2) Router → 402 Payment Required
{
"x402Version": 1,
"error": "payment_required",
"accepts": [
{"scheme":"exact","network":"base","asset":"0x8335…2913","asset_symbol":"USDC",
"max_amount_required":"20000","pay_to":"0xDeineWallet","resource":"https://…/extract-clean", …},
{"scheme":"exact","network":"base","asset":"0xfde4…9bb2","asset_symbol":"USDT",
"max_amount_required":"20000", …}
],
"facilitator": "https://facilitator.payai.network/verify",
"free_tier": {"limit": 3, "how_to": "Send header X-WALLET: 0x… for free requests"}
}
3) Agent wählt USDC ODER USDT, signiert eine EIP-3009 transferWithAuthorization
(from = Agent, to = pay_to, value ≥ max_amount_required, nonce = zufällig)
4) Agent → POST /extract-clean mit X-PAYMENT: <base64(JSON-Payload)>
optional: X-PAYMENT-TOKEN: USDT (spart einen Verify-Roundtrip)
5) Router: lokale Checks (Netzwerk, Empfänger, Betrag, Replay-Nonce in Redis)
→ Facilitator /verify (erst USDC-Requirements, dann USDT, bzw. gemäß Hint)
6) Router führt Request aus → 200 OK + Daten
7) Router → Facilitator /settle (Transfer wird on-chain ausgeführt)
Ergebnis im Header X-PAYMENT-RESPONSE (base64 JSON: success, transaction, payer)
```
**Free Tier:** Header `X-WALLET: 0x…` gewährt `FREE_TIER_LIMIT` Requests pro Wallet (Zähler in Redis, 30 Tage). Zusätzlich gilt ein IP-Deckel von 20 Free-Requests/Tag gegen Wallet-Enumeration. Der Header `X-Free-Tier-Remaining` zeigt den Reststand.
**Sicherheit:** Nonce-Replay-Schutz (Redis, `PAYMENT_REPLAY_TTL_SECONDS`), Betrags- und Empfängerprüfung *vor* dem Facilitator-Call, Secrets erscheinen nie in Logs (Scrubbing im JSON-Logger), Settlement erst nach erfolgreicher Auslieferung – ein fehlgeschlagenes Settlement wird als `payment.settlement_failed_after_delivery` geloggt.
Beispiel mit dem offiziellen x402-Client (TypeScript):
```ts
import { wrapFetchWithPayment } from "x402-fetch";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.AGENT_PK as `0x${string}`);
const fetchWithPay = wrapFetchWithPayment(fetch, account);
const res = await fetchWithPay("https://api.deine-domain.tld/market-spread", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ asset: "ETH", buy_venue: "binance", sell_venue: "coinbase" }),
});
console.log(await res.json());
```
---
## 8. Provider hinzufügen
1. Adapter anlegen, z. B. `app/services/providers/firecrawl_provider.py`:
```python
import httpx
from app.models.provider import ProviderResult
from app.services.providers.base import BaseProvider, ProviderError
class FirecrawlProvider(BaseProvider):
@property
def is_configured(self) -> bool:
return bool(self.api_key) # ohne Key wird der Provider nicht geroutet
async def _execute(self, request: dict) -> ProviderResult:
r = await self.http.post( # gemeinsamer httpx.AsyncClient aus BaseProvider
"https://api.firecrawl.dev/v1/scrape",
headers={"Authorization": f"Bearer {self.api_key}"},
json={"url": request["url"], "formats": ["extract"], "extract": {"schema": request.get("schema")}},
)
if r.status_code >= 400:
raise ProviderError(self.name, f"HTTP {r.status_code}", retryable=r.status_code >= 500)
payload = r.json()
return ProviderResult(
provider=self.name,
data=payload["data"]["extract"],
confidence=0.9,
sources_checked=1,
freshness_seconds=0,
)
async def _health_probe(self) -> bool:
return bool(self.api_key) # oder ein günstiger Ping-Endpunkt
```
2. In `app/services/providers/__init__.py` → `build_default_providers()` registrieren:
```python
FirecrawlProvider(
ProviderConfig(
name="firecrawl",
capability=Capability.EXTRACT,
cost_per_request=0.005, # echte Kosten pro Call → fließt in Scoring + Margenprüfung
base_quality=0.93,
expected_latency_ms=1500,
timeout_seconds=15,
license=LicensePolicy(
allowed_use=["derived_output"], max_cache_seconds=300, commercial_resale=True, contract_expiry=None
),
),
api_key=settings.firecrawl_api_key,
)
```
3. Neues Feld in `app/config.py` anlegen (z. B. `firecrawl_api_key: str = ""`), Key in `.env` setzen (`FIRECRAWL_API_KEY=...`), Tests mit dem `UpstreamStub` aus `tests/conftest.py` ergänzen, deployen.
Der Router übernimmt automatisch: Scoring, Fallback-Reihenfolge, Circuit-Breaker, Tagesbudget, Lizenzprüfung (`commercial_resale`, `contract_expiry`) und Cache-TTL-Begrenzung (`max_cache_seconds`).
Für `/market-spread` muss `data` die Felder `buy_price`, `sell_price`, `buy_fee_bps`, `sell_fee_bps`, `buy_liquidity_usd`, `sell_liquidity_usd`, `transfer_cost`, `transfer_minutes` liefern (siehe Docstring in `defi_provider.py`).
---
## 9. Tests & Projektstruktur
```bash
pip install -r requirements-dev.txt
pytest -q # 80 Tests: kostenlose Exchange-Feeds (Binance/Coinbase/DeFi Llama), Payment (USDC/USDT/Free Tier/Replay), Routing, Circuit-Breaker, Marge, Cache, Spread-Mathematik,
# API-Keys, Sliding-Window-Rate-Limiting, Usage-Tracking, /stats
ruff check .
```
Tests benötigen **keinen** laufenden Redis (fakeredis) und **keinen** echten Facilitator (httpx MockTransport).
```text
agent_margin_router/
├── app/
│ ├── main.py # App-Factory, CORS, Lifespan (Redis connect/disconnect), Metriken
│ ├── config.py # pydantic-settings (.env)
│ ├── routers/ # /extract-clean, /market-spread, /health, enterprise.py (/api-keys, /usage, /stats)
│ ├── middleware/x402.py # API-Key-Auflösung, Rate-Limits, 402-Challenge, Free Tier, Verify/Settle
│ ├── services/
│ │ ├── provider_router.py # Scoring, Fallback, Circuit-Breaker, Mindestmarge, Tagesbudget
│ │ ├── cache.py # redis.asyncio Wrapper, Hit/Miss, Free-Tier-Zähler, Replay-Schutz
│ │ ├── payment.py # x402 Facilitator-Client (USDC + USDT)
│ │ ├── api_keys.py # API-Key-Erzeugung/-Auflösung/-Deaktivierung (Redis)
│ │ ├── rate_limit.py # Sliding-Window-Limiter (Redis ZSET), X-RateLimit-Header
│ │ ├── usage.py # Usage-Tracking pro Wallet/Key + globale Stats
│ │ └── providers/ # base.py, scraping_provider.py, defi_provider.py
│ ├── models/ # Pydantic v2 Request/Response/Provider-Modelle
│ └── utils/logging.py # JSON-Logs mit Secret-Scrubbing
├── tests/
├── docker-compose.yml # app + redis (+ cloudflared via --profile tunnel)
├── Dockerfile # Multi-Stage, non-root, Healthcheck
├── .env.example
├── requirements.txt / requirements-dev.txt / pyproject.toml
└── README.md
```
---
**Rechtlicher Hinweis:** Dieses Projekt ist eine technische Infrastruktur. Provider-Lizenzen, Datenschutz, Steuer- und Zahlungsrecht sind vor dem Produktivbetrieb eigenständig zu prüfen. Es werden keine Umsätze garantiert.