{
  "markdown": "# Agent Margin Router\n\n[![Live API](https://img.shields.io/badge/API-live-22c55e)](https://agent-margin-router-production.up.railway.app/health)\n[![Docs](https://img.shields.io/badge/docs-landing%20page-06b6d4)](https://agentmarginrouter.com)\n[![x402](https://img.shields.io/badge/payments-x402%20%7C%20USDC%20%2B%20USDT%20on%20Base-8b5cf6)](https://www.x402.org)\n[![Python](https://img.shields.io/badge/python-3.11%2B-3776ab)](#)\n[![Tests](https://img.shields.io/badge/tests-80%20passing-22c55e)](#9-tests--projektstruktur)\n\n> **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.\n>\n> - **Live API:** `https://agent-margin-router-production.up.railway.app`\n> - **Docs & examples:** https://agentmarginrouter.com\n> - **Endpoints:** `POST /extract-clean` (URL → clean JSON, $0.02) · `POST /market-spread` (net spread between two venues after fees, $0.05) · `GET /health` (free)\n>\n> ```bash\n> # Try it free (3 requests per wallet):\n> curl -X POST https://agent-margin-router-production.up.railway.app/market-spread \\\n>   -H \"Content-Type: application/json\" -H \"X-WALLET: 0xYourWallet\" \\\n>   -d '{\"asset\":\"ethereum\",\"buy_venue\":\"binance\",\"sell_venue\":\"coinbase\"}'\n> ```\n>\n> Full German documentation below · Vollständige deutsche Dokumentation folgt.\n\n> Einheitliche, per **x402** bezahlte Routing-Schicht für AI-Agenten.\n> Mehrere Daten-Provider → automatische Auswahl (Qualität, Preis, Latenz, Ausfallrisiko) → normalisiertes JSON → Bezahlung pro Request in **USDC oder USDT auf Base**.\n\n```text\nAgent  ──POST /extract-clean──▶  Router ──▶ Provider-Scoring ──▶ bester Provider\n  ▲                                │            (Fallback, Circuit-Breaker, Mindestmarge)\n  │  402 Payment Required          │\n  │  (USDC + USDT Optionen)        ▼\n  └──X-PAYMENT (signiert)──▶  Facilitator /verify  ──▶ 200 OK + Daten  ──▶ /settle on-chain\n```\n\n## Inhalt\n\n1. [Was ist der Agent Margin Router](#1-was-ist-der-agent-margin-router)\n2. [Schnellstart (lokal ohne Docker)](#2-schnellstart-lokal-ohne-docker)\n3. [Deployment mit Docker-Compose auf Hetzner](#3-deployment-mit-docker-compose-auf-hetzner)\n4. [Cloudflare Tunnel Setup](#4-cloudflare-tunnel-setup)\n4b. [Deployment auf Railway](#4b-deployment-auf-railway)\n5. [Umgebungsvariablen](#5-umgebungsvariablen)\n6. [API-Endpunkte mit Beispielen](#6-api-endpunkte)\n6b. [Enterprise: API-Keys, Rate-Limiting, Usage & Stats](#6b-enterprise-api-keys-rate-limiting-usage--stats)\n7. [x402 Payment Flow](#7-x402-payment-flow)\n8. [Provider hinzufügen](#8-provider-hinzufügen)\n9. [Tests & Projektstruktur](#9-tests--projektstruktur)\n\n---\n\n## 1. Was ist der Agent Margin Router\n\nDer Router ist ein **B2B-Daten-Broker für Maschinen**. Ein AI-Agent stellt eine standardisierte Anfrage, der Router\n\n1. prüft den Cache (Redis),\n2. bewertet alle passenden Provider mit `score = quality × success_probability / (price + latency_risk)`,\n3. führt den Request nur aus, wenn die **Mindestmarge** (Default 20 %) erhalten bleibt,\n4. fällt bei Fehlern automatisch auf den nächsten Provider zurück (**Circuit-Breaker**: 3 Fehler → 5 Minuten Pause),\n5. normalisiert die Antwort und liefert sie mit Aktualitäts- und Konfidenzangabe aus,\n6. protokolliert Kosten, Provider und Marge als strukturierte JSON-Logs.\n\nBezahlt 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).\n\n**MVP-Endpunkte:**\n\n| Endpunkt | Funktion | Preis (Default) |\n|---|---|---|\n| `POST /extract-clean` | URL → sauberes, schema-konformes JSON | 0,02 USD |\n| `POST /market-spread` | Netto-Spread zwischen zwei Handelsplätzen nach Gebühren, Slippage & Transfer | 0,05 USD |\n| `GET /health` | Status, Redis, Provider, Uptime, Fehlerrate, Cache-Hit-Rate | kostenlos |\n\n> **Provider (echte APIs):**\n>\n> | Name | Capability | Anbieter | Kosten/Call (Default) | Qualität |\n> |---|---|---|---|---|\n> | `apify_web_scraper` | extract | Apify Actor `apify/web-scraper` (Headless-Browser, optional Proxy) | ~0,005 USD (wird aus `usageTotalUsd` nachjustiert) | 0.90 |\n> | `httpx_direct` | extract | direkter HTTP-Abruf, kein JS | ~0,0002 USD | 0.60 |\n> | `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 |\n> | `coinbase_public` | market_data | Coinbase Public API `/prices/{pair}/spot` | 0 USD, kein Key | 0.90 |\n> | `coingecko` | market_data | CoinGecko `/simple/price`, `/coins/{id}/tickers`, optional `/market_chart` | 0 USD (Demo-Key, Rate-Limit) | 0.88 |\n> | `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 |\n> | `coinmarketcap` | market_data | CMC `/cryptocurrency/quotes/latest` (Fallback, nur Referenzpreis) | ~0,0004 USD (Credits) | 0.70 |\n>\n> Ohne gesetzten API-Key wird ein Provider **nicht geroutet** (`is_configured == False`), taucht aber in `/health` mit `configured: false` auf.\n> 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.\n> `/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.\n>\n> **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.\n\n---\n\n## 2. Schnellstart (lokal ohne Docker)\n\nVoraussetzungen: Python ≥ 3.11, optional ein lokaler Redis (ohne Redis nutzt die App im Dev-Modus automatisch einen In-Memory-Fallback).\n\n```bash\ngit clone <dein-repo> agent_margin_router\ncd agent_margin_router\n\npython -m venv .venv && source .venv/bin/activate\npip install -r requirements-dev.txt\n\ncp .env.example .env\n# Für lokale Tests ohne Wallet/Zahlung:\nsed -i 's/^PAYMENT_ENABLED=.*/PAYMENT_ENABLED=false/' .env\nsed -i 's#^REDIS_URL=.*#REDIS_URL=redis://localhost:6379/0#' .env\n\nuvicorn app.main:app --reload --port 8000\n```\n\nDann:\n\n```bash\ncurl -s localhost:8000/health | jq .status\ncurl -s -X POST localhost:8000/extract-clean \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"url\":\"https://example.com/produkt/123\"}' | jq\n```\n\nInteraktive API-Doku: <http://localhost:8000/docs>\n\nTests:\n\n```bash\npytest -q\n```\n\n---\n\n## 3. Deployment mit Docker-Compose auf Hetzner\n\nGetestet für einen Hetzner Cloud Server (CX22 reicht für den Start, Ubuntu 24.04).\n\n### 3.1 Server vorbereiten\n\n```bash\nssh root@<SERVER-IP>\n\n# System aktualisieren, Docker installieren\napt update && apt upgrade -y\ncurl -fsSL https://get.docker.com | sh\n\n# Nicht-Root-User für den Betrieb\nadduser --disabled-password --gecos \"\" deploy\nusermod -aG docker deploy\n\n# Firewall: nur SSH offen lassen (HTTP kommt über Cloudflare Tunnel, s. Abschnitt 4)\napt install -y ufw\nufw allow OpenSSH\nufw --force enable\n```\n\n### 3.2 Projekt deployen\n\n```bash\nsu - deploy\ngit clone <dein-repo> agent_margin_router\ncd agent_margin_router\n\ncp .env.example .env\nnano .env        # RECEIVER_WALLET, ENVIRONMENT=production, REDIS_REQUIRED=true, Preise, Provider-Keys\n```\n\nWichtige Produktionswerte in `.env`:\n\n```dotenv\nENVIRONMENT=production\nREDIS_URL=redis://redis:6379/0\nREDIS_REQUIRED=true\nPAYMENT_ENABLED=true\nRECEIVER_WALLET=0xDeineEchteWallet\n```\n\nStarten:\n\n```bash\ndocker compose up -d --build\ndocker compose ps\ndocker compose logs -f app\ncurl -s localhost:8000/health | jq\n```\n\nDer 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.\n\n### 3.3 Betrieb\n\n```bash\ndocker compose pull && docker compose up -d --build   # Update\ndocker compose logs --tail=200 app                    # JSON-Logs\ndocker compose exec redis redis-cli INFO memory       # Redis prüfen\ndocker compose down                                   # Stoppen (Redis-Daten bleiben im Volume)\n```\n\nBeide Services haben Healthchecks und `restart: unless-stopped`; nach einem Server-Reboot startet der Stack automatisch.\n\n---\n\n## 4. Cloudflare Tunnel Setup\n\nMit einem Cloudflare Tunnel braucht der Hetzner-Server **keinen offenen HTTP-Port**; TLS, DDoS-Schutz und WAF übernimmt Cloudflare.\n\n1. Domain bei Cloudflare verwalten (Nameserver umstellen).\n2. Cloudflare Dashboard → **Zero Trust** → **Networks** → **Tunnels** → **Create a tunnel** (Typ „Cloudflared“).\n3. Namen vergeben (z. B. `agent-margin-router`) und den angezeigten **Token** kopieren.\n4. Im Tunnel unter **Public Hostname** einen Eintrag anlegen:\n   - Subdomain: `api`, Domain: `deine-domain.tld`\n   - Service: `HTTP` → `app:8000` (Docker-Servicename, da cloudflared im selben Compose-Netz läuft)\n5. Token in `.env` eintragen:\n\n   ```dotenv\n   CLOUDFLARE_TUNNEL_TOKEN=eyJhIjoi...\n   ```\n\n6. Stack mit Tunnel-Profil starten:\n\n   ```bash\n   docker compose --profile tunnel up -d\n   docker compose logs -f cloudflared      # \"Registered tunnel connection\" = erfolgreich\n   curl -s https://api.deine-domain.tld/health | jq .status\n   ```\n\nEmpfohlen 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.\n\n---\n\n## 4b. Deployment auf Railway\n\nAlternative zu Hetzner/Docker-Compose. Das Repo enthält `railway.toml` (Dockerfile-Build, Healthcheck `/health`, Restart-Policy) und ein `Procfile`.\n\n1. Neues Railway-Projekt → **Deploy from GitHub repo** (oder `railway up` per CLI).\n2. **Redis-Service** hinzufügen (Datenbanken → Redis) und im App-Service die Variable `REDIS_URL=${{Redis.REDIS_URL}}` setzen; `REDIS_REQUIRED=true`.\n3. 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.\n4. Deploy abwarten, dann `curl https://<railway-domain>/health` – im Feld `providers` müssen die konfigurierten Provider `configured: true` melden.\n\nHinweis: Der Dockerfile-Start (`uvicorn ... --port ${PORT:-8000}`) läuft unverändert auch lokal und in docker-compose (dort bleibt Port 8000).\n\n---\n\n## 5. Umgebungsvariablen\n\nAlle Variablen sind in [`.env.example`](.env.example) mit Kommentaren dokumentiert. Die wichtigsten:\n\n| Variable | Bedeutung | Default |\n|---|---|---|\n| `REDIS_URL` | Redis-Verbindung (Cache, Free-Tier-Zähler, Replay-Schutz) | `redis://redis:6379/0` |\n| `REDIS_REQUIRED` | `true`: kein Start ohne Redis; `false`: In-Memory-Fallback (nur Dev) | `false` |\n| `PAYMENT_ENABLED` | `false` schaltet x402 komplett ab (nur lokal!) | `true` |\n| `FACILITATOR_URL` | x402-Facilitator `/verify`-Endpunkt (PayAI) | facilitator.payai.network |\n| `USDC_CONTRACT_ADDRESS` | USDC auf Base | `0x8335…2913` |\n| `USDT_CONTRACT_ADDRESS` | USDT auf Base | `0xfde4…9bb2` |\n| `PAYMENT_NETWORK` | x402-Netzwerkname (`base`, `base-sepolia`) | `base` |\n| `RECEIVER_WALLET` | **Deine** Einnahmen-Wallet | – |\n| `EXTRACT_PRICE` / `MARKET_SPREAD_PRICE` | Verkaufspreise in USD | `0.02` / `0.05` |\n| `FREE_TIER_LIMIT` | Kostenlose Requests pro Wallet | `3` |\n| `MIN_MARGIN_PERCENT` | Mindestmarge; darunter wird nicht ausgeführt | `20` |\n| `DAILY_PROVIDER_BUDGET_USD` | Tages-Kostendeckel pro Provider | `50` |\n| `CACHE_TTL_SECONDS` | Standard-Cache-Dauer | `60` |\n| `CIRCUIT_FAILURE_THRESHOLD` / `CIRCUIT_OPEN_SECONDS` | Circuit-Breaker | `3` / `300` |\n| `ENABLED_TOKENS` | Akzeptierte Zahl-Token, kommagetrennt (`USDC` bis USDT real E2E getestet ist) | `USDC,USDT` |\n| `APIFY_API_TOKEN` | Apify-Token für `apify_web_scraper` (leer = Provider aus) | leer |\n| `APIFY_ACTOR_ID` / `APIFY_TIMEOUT_SECONDS` / `APIFY_USE_PROXY` | Actor, Run-Timeout, Apify-Proxy | `apify~web-scraper` / `90` / `true` |\n| `APIFY_COST_PER_REQUEST` | Startannahme Kosten/Call (wird aus Run-Usage nachgeführt) | `0.005` |\n| `COINGECKO_API_KEY` | CoinGecko Demo/Pro-Key für `coingecko` | leer |\n| `COINGECKO_USE_MARKET_CHART` | zusätzlich 24h-Volatilität abrufen (1 Call mehr) | `true` |\n| `CMC_API_KEY` | CoinMarketCap-Key für Fallback `coinmarketcap` | leer |\n| `HTTPX_FALLBACK_ENABLED` | günstigen `httpx_direct`-Fallback registrieren | `true` |\n| `FREE_TIER_MAX_PROVIDER_COST_USD` | Kostendeckel je Call für Free-Tier-Requests | `0.001` |\n| `PAID_MIN_QUALITY` | bezahlte Requests bevorzugen Provider ab dieser Qualität | `0.8` |\n| `API_KEY_RATE_LIMIT_PER_MINUTE` / `API_KEY_RATE_LIMIT_MAX` | Standard-Limit pro API-Key je Fenster / harter Deckel | `100` / `1000` |\n| `ANON_RATE_LIMIT_PER_MINUTE` | Limit pro IP ohne API-Key und ohne `X-WALLET` | `10` |\n| `RATE_LIMIT_WINDOW_SECONDS` | Länge des Sliding Windows | `60` |\n| `API_KEY_ADMIN_SECRET` | Wenn gesetzt: `X-ADMIN-SECRET` nötig für Key-Erzeugung/-Deaktivierung (**in Produktion setzen**) | leer |\n| `USAGE_RECENT_LIMIT` / `USAGE_DAILY_TTL_SECONDS` | Letzte Requests pro Identität / Aufbewahrung der Tageswerte | `20` / `172800` |\n| `CLOUDFLARE_TUNNEL_TOKEN` | Nur für `--profile tunnel` | leer |\n\nFü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.\n\n---\n\n## 6. API-Endpunkte\n\n### `GET /health` (kostenlos)\n\n```bash\ncurl -s https://api.deine-domain.tld/health | jq\n```\n\n```json\n{\n  \"status\": \"ok\",\n  \"version\": \"0.1.0\",\n  \"environment\": \"production\",\n  \"timestamp\": \"2026-09-10T12:00:00Z\",\n  \"uptime_seconds\": 86400.2,\n  \"redis_connected\": true,\n  \"redis_backend\": \"redis\",\n  \"request_count\": 1520,\n  \"error_count\": 3,\n  \"error_rate\": 0.002,\n  \"cache_hits\": 610,\n  \"cache_misses\": 900,\n  \"cache_hit_rate\": 0.404,\n  \"providers\": [\n    {\"name\": \"apify_web_scraper\", \"capability\": \"extract\", \"healthy\": true, \"circuit_open\": false,\n     \"consecutive_failures\": 0, \"success_rate\": 0.98, \"avg_latency_ms\": 61.2, \"requests\": 700}\n  ]\n}\n```\n\n`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).\n\n### `POST /extract-clean` (0,02 USD)\n\nRequest:\n\n```bash\ncurl -s -X POST https://api.deine-domain.tld/extract-clean \\\n  -H 'Content-Type: application/json' \\\n  -H 'X-WALLET: 0xDeineWallet' \\\n  -d '{\n        \"url\": \"https://shop.example.com/produkt/123\",\n        \"schema\": {\"type\":\"object\",\"properties\":{\"title\":{},\"price\":{},\"currency\":{},\"availability\":{}}},\n        \"max_age_seconds\": 60\n      }' | jq\n```\n\nResponse:\n\n```json\n{\n  \"data\": {\"title\": \"Shop – Product Overview\", \"price\": 149.9, \"currency\": \"EUR\", \"availability\": \"in_stock\"},\n  \"sources_checked\": 1,\n  \"freshness_seconds\": 0,\n  \"confidence\": 0.91,\n  \"cost_usdc\": 0.02,\n  \"token\": \"USDC\",\n  \"fetched_at\": \"2026-09-10T12:00:01Z\",\n  \"routing\": {\"provider\": \"apify_web_scraper\", \"fallback_used\": false,\n              \"providers_tried\": [\"apify_web_scraper\"], \"cache_hit\": false, \"latency_ms\": 74}\n}\n```\n\nFelder: `schema` (optional) formt die Ausgabe auf die Top-Level-Properties; `max_age_seconds: 0` erzwingt einen frischen Abruf.\n\n### `POST /market-spread` (0,05 USD)\n\n```bash\ncurl -s -X POST https://api.deine-domain.tld/market-spread \\\n  -H 'Content-Type: application/json' \\\n  -H 'X-WALLET: 0xDeineWallet' \\\n  -d '{\"asset\":\"ETH\",\"buy_venue\":\"binance\",\"sell_venue\":\"coinbase\",\"size_usd\":5000}' | jq\n```\n\n```json\n{\n  \"asset\": \"ETH\", \"quote\": \"USDT\", \"buy_venue\": \"binance\", \"sell_venue\": \"coinbase\",\n  \"buy_price\": 4247.31, \"sell_price\": 4258.02,\n  \"gross_spread_bps\": 25.2, \"fees_bps\": 70.0, \"slippage_bps\": 3.4,\n  \"transfer_cost\": 1.31, \"transfer_cost_bps\": 2.62,\n  \"net_spread_bps\": -50.8, \"estimated_net_profit_usd\": -25.4,\n  \"confidence\": 0.86, \"execution_risk\": \"high\",\n  \"freshness_seconds\": 4, \"cost_usdc\": 0.05, \"token\": \"USDT\",\n  \"fetched_at\": \"2026-09-10T12:00:02Z\",\n  \"routing\": {\"provider\": \"coingecko\", \"fallback_used\": false, \"providers_tried\": [\"coingecko\"], \"cache_hit\": false, \"latency_ms\": 58}\n}\n```\n\n`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.\n\n### Fehlerformat\n\n```json\n{\"error\": \"http_error\", \"detail\": \"No extraction provider available: ...\"}\n```\n\n| Status | Bedeutung |\n|---|---|\n| `402` | Zahlung fehlt/ungültig oder Free Tier erschöpft (Body enthält Zahlungsoptionen) |\n| `422` | Validierungsfehler im Request |\n| `502` | Facilitator nicht erreichbar / Provider lieferte unbrauchbare Daten |\n| `401` | `X-API-KEY` unbekannt oder deaktiviert (`{\"error\": \"invalid_api_key\"}`) |\n| `429` | Rate-Limit überschritten (`{\"error\": \"rate_limit_exceeded\", \"retry_after\": …}`, Header `Retry-After`) |\n| `503` | Kein Provider verfügbar oder Mindestmarge verletzt |\n\n---\n\n## 6b. Enterprise: API-Keys, Rate-Limiting, Usage & Stats\n\nAlle Endpunkte in diesem Abschnitt sind **kostenlos** (kein x402). Persistenz komplett in Redis; ohne Redis\n(`REDIS_REQUIRED=false`) greift derselbe In-Memory-Fallback wie beim Cache.\n\n### Authentifizierung per API-Key\n\nEin API-Key (UUID4) ist fest an eine Wallet gebunden. Wird der Header `X-API-KEY` gesendet, ermittelt der Router\ndie Wallet automatisch – `X-WALLET` ist dann nicht mehr nötig. Free Tier und x402-Zahlung gelten unverändert\n(der Key ersetzt nur die Identifikation und bringt ein eigenes Rate-Limit mit).\n\n```bash\n# Key erzeugen (Label und Limit optional; Limit wird auf API_KEY_RATE_LIMIT_MAX gedeckelt)\ncurl -s -X POST https://<host>/api-keys/generate \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"wallet\": \"0xabc…\", \"label\": \"trading-bot-1\", \"rate_limit_per_minute\": 300}'\n# → 201 {\"api_key\": \"6f1c…\", \"wallet\": \"0xabc…\", \"label\": \"trading-bot-1\", \"rate_limit_per_minute\": 300, \"active\": true, …}\n\n# Verwenden\ncurl -s -X POST https://<host>/extract-clean -H 'X-API-KEY: 6f1c…' \\\n  -H 'Content-Type: application/json' -d '{\"url\": \"https://example.com\"}'\n\n# Key ansehen / deaktivieren (Deaktivierung ist idempotent; Usage-Historie bleibt erhalten)\ncurl -s https://<host>/api-keys/6f1c…\ncurl -s -X DELETE https://<host>/api-keys/6f1c…\n```\n\nIst `API_KEY_ADMIN_SECRET` gesetzt, verlangen `POST /api-keys/generate` und `DELETE /api-keys/{key}` zusätzlich\nden Header `X-ADMIN-SECRET: <secret>` (sonst `403`). Ohne Secret kann jeder Keys anlegen – nur für Entwicklung.\n\n### Rate-Limiting (Sliding Window, Redis ZSET)\n\n| Aufrufer | Identifikation | Limit | Quelle |\n|---|---|---|---|\n| API-Key | `X-API-KEY` | `rate_limit_per_minute` des Keys (Default `API_KEY_RATE_LIMIT_PER_MINUTE` = 100) | pro Key |\n| Wallet | `X-WALLET` | Free Tier: `FREE_TIER_LIMIT` (3) Requests pro Wallet, danach x402 | pro Wallet |\n| Anonym | keins | `ANON_RATE_LIMIT_PER_MINUTE` (10) pro Client-IP | pro IP |\n\nJede Antwort eines bezahlten Endpunkts (`/extract-clean`, `/market-spread`) – auch `402`/`429` – trägt:\n\n```\nX-RateLimit-Limit: 100        # Limit des Fensters (bei X-WALLET: Free-Tier-Limit)\nX-RateLimit-Remaining: 97     # verbleibende Requests im aktuellen Fenster\nX-RateLimit-Reset: 1789000000 # Unix-Timestamp, ab dem wieder Kontingent frei wird\nRetry-After: 42               # nur bei 429\n```\n\nDas Fenster ist ein echtes Sliding Window (`RATE_LIMIT_WINDOW_SECONDS`, Default 60 s): jeder Request wird mit\nZeitstempel in ein Redis-ZSET geschrieben, ältere Einträge fallen heraus. Bei Redis-Fehlern öffnet der Limiter\n(fail-open) und loggt eine Warnung – zahlende Kunden werden nie wegen eines Cache-Problems blockiert.\nEin `429` verbraucht **kein** Free-Tier-Kontingent.\n\n### Usage-Tracking\n\nJeder erfolgreiche Request auf einem bezahlten Endpunkt wird pro Identität (`wallet:0x…` bzw. `key:<uuid>`)\nund global gezählt: Requests (heute/gesamt, bezahlt/frei, Cache-Hits), gezahlte Kosten, Provider-Kosten,\nProvider- und Endpunkt-Verteilung sowie die letzten `USAGE_RECENT_LIMIT` Requests.\n\n```bash\ncurl -s 'https://<host>/usage?wallet=0xabc…'      # oder ?api_key=6f1c…\n```\n\n```json\n{\n  \"identity_type\": \"api_key\", \"wallet\": \"0xabc…\", \"api_key\": \"6f1c…\", \"label\": \"trading-bot-1\", \"api_key_active\": true,\n  \"requests_today\": 12, \"requests_total\": 340, \"paid_requests_total\": 337, \"free_requests_total\": 3, \"cache_hits_total\": 41,\n  \"cost_usd_today\": 0.24, \"cost_usd_total\": 6.74,\n  \"providers\": {\"coingecko\": 300, \"coinmarketcap\": 40}, \"endpoints\": {\"/market-spread\": 340},\n  \"free_tier\": {\"limit\": 3, \"used\": 3, \"remaining\": 0},\n  \"rate_limit\": {\"limit\": 300, \"remaining\": 298, \"window_seconds\": 60, \"reset\": 1789000060},\n  \"last_request_at\": \"2026-09-10T10:00:00+00:00\",\n  \"last_requests\": [{\"at\": \"…\", \"path\": \"/market-spread\", \"provider\": \"coingecko\", \"mode\": \"paid\", \"token\": \"USDC\",\n                     \"price_usd\": 0.05, \"provider_cost_usd\": 0.0, \"cache_hit\": false, \"latency_ms\": 412, \"status\": 200}]\n}\n```\n\nHinweise: Usage per API-Key und per nackter Wallet werden getrennt geführt (der Key ist die Identität), der\n**Free Tier ist jedoch wallet-gebunden** und damit geteilt. Anonyme x402-Zahlungen werden der Payer-Wallet\naus der Facilitator-Verifikation zugeordnet.\n\n### Globale Statistiken\n\n```bash\ncurl -s https://<host>/stats\n```\n\n```json\n{\n  \"requests_total\": 10234, \"requests_today\": 311, \"paid_requests_total\": 9800, \"free_requests_total\": 434,\n  \"cache_hits_total\": 1200, \"active_wallets_total\": 87, \"active_wallets_today\": 14, \"active_api_keys\": 23,\n  \"revenue_usd_total\": 312.55, \"revenue_usd_today\": 9.10, \"provider_cost_usd_total\": 41.02,\n  \"gross_margin_usd_total\": 271.53, \"providers\": {\"coingecko\": 7000, \"httpx_direct\": 2100, \"apify_web_scraper\": 1134},\n  \"endpoints\": {\"/market-spread\": 7000, \"/extract-clean\": 3234}, \"last_request_at\": \"…\", \"generated_at\": \"…\"\n}\n```\n\n`revenue_usd_*` summiert nur tatsächlich per x402 bezahlte Requests; `active_wallets_*` zählt eindeutige Wallets\n(API-Key-Wallets, `X-WALLET`-Wallets und Payer-Wallets).\n\n---\n\n## 7. x402 Payment Flow\n\n```text\n1) Agent → POST /extract-clean                       (ohne X-PAYMENT)\n2) Router → 402 Payment Required\n   {\n     \"x402Version\": 1,\n     \"error\": \"payment_required\",\n     \"accepts\": [\n       {\"scheme\":\"exact\",\"network\":\"base\",\"asset\":\"0x8335…2913\",\"asset_symbol\":\"USDC\",\n        \"max_amount_required\":\"20000\",\"pay_to\":\"0xDeineWallet\",\"resource\":\"https://…/extract-clean\", …},\n       {\"scheme\":\"exact\",\"network\":\"base\",\"asset\":\"0xfde4…9bb2\",\"asset_symbol\":\"USDT\",\n        \"max_amount_required\":\"20000\", …}\n     ],\n     \"facilitator\": \"https://facilitator.payai.network/verify\",\n     \"free_tier\": {\"limit\": 3, \"how_to\": \"Send header X-WALLET: 0x… for free requests\"}\n   }\n3) Agent wählt USDC ODER USDT, signiert eine EIP-3009 transferWithAuthorization\n   (from = Agent, to = pay_to, value ≥ max_amount_required, nonce = zufällig)\n4) Agent → POST /extract-clean  mit  X-PAYMENT: <base64(JSON-Payload)>\n   optional: X-PAYMENT-TOKEN: USDT   (spart einen Verify-Roundtrip)\n5) Router: lokale Checks (Netzwerk, Empfänger, Betrag, Replay-Nonce in Redis)\n           → Facilitator /verify (erst USDC-Requirements, dann USDT, bzw. gemäß Hint)\n6) Router führt Request aus → 200 OK + Daten\n7) Router → Facilitator /settle (Transfer wird on-chain ausgeführt)\n           Ergebnis im Header X-PAYMENT-RESPONSE (base64 JSON: success, transaction, payer)\n```\n\n**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.\n\n**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.\n\nBeispiel mit dem offiziellen x402-Client (TypeScript):\n\n```ts\nimport { wrapFetchWithPayment } from \"x402-fetch\";\nimport { privateKeyToAccount } from \"viem/accounts\";\n\nconst account = privateKeyToAccount(process.env.AGENT_PK as `0x${string}`);\nconst fetchWithPay = wrapFetchWithPayment(fetch, account);\n\nconst res = await fetchWithPay(\"https://api.deine-domain.tld/market-spread\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ asset: \"ETH\", buy_venue: \"binance\", sell_venue: \"coinbase\" }),\n});\nconsole.log(await res.json());\n```\n\n---\n\n## 8. Provider hinzufügen\n\n1. Adapter anlegen, z. B. `app/services/providers/firecrawl_provider.py`:\n\n   ```python\n   import httpx\n   from app.models.provider import ProviderResult\n   from app.services.providers.base import BaseProvider, ProviderError\n\n\n   class FirecrawlProvider(BaseProvider):\n       @property\n       def is_configured(self) -> bool:\n           return bool(self.api_key)  # ohne Key wird der Provider nicht geroutet\n\n       async def _execute(self, request: dict) -> ProviderResult:\n           r = await self.http.post(  # gemeinsamer httpx.AsyncClient aus BaseProvider\n               \"https://api.firecrawl.dev/v1/scrape\",\n               headers={\"Authorization\": f\"Bearer {self.api_key}\"},\n               json={\"url\": request[\"url\"], \"formats\": [\"extract\"], \"extract\": {\"schema\": request.get(\"schema\")}},\n           )\n           if r.status_code >= 400:\n               raise ProviderError(self.name, f\"HTTP {r.status_code}\", retryable=r.status_code >= 500)\n           payload = r.json()\n           return ProviderResult(\n               provider=self.name,\n               data=payload[\"data\"][\"extract\"],\n               confidence=0.9,\n               sources_checked=1,\n               freshness_seconds=0,\n           )\n\n       async def _health_probe(self) -> bool:\n           return bool(self.api_key)  # oder ein günstiger Ping-Endpunkt\n   ```\n\n2. In `app/services/providers/__init__.py` → `build_default_providers()` registrieren:\n\n   ```python\n   FirecrawlProvider(\n       ProviderConfig(\n           name=\"firecrawl\",\n           capability=Capability.EXTRACT,\n           cost_per_request=0.005,  # echte Kosten pro Call → fließt in Scoring + Margenprüfung\n           base_quality=0.93,\n           expected_latency_ms=1500,\n           timeout_seconds=15,\n           license=LicensePolicy(\n               allowed_use=[\"derived_output\"], max_cache_seconds=300, commercial_resale=True, contract_expiry=None\n           ),\n       ),\n       api_key=settings.firecrawl_api_key,\n   )\n   ```\n\n3. 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.\n\nDer Router übernimmt automatisch: Scoring, Fallback-Reihenfolge, Circuit-Breaker, Tagesbudget, Lizenzprüfung (`commercial_resale`, `contract_expiry`) und Cache-TTL-Begrenzung (`max_cache_seconds`).\n\nFü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`).\n\n---\n\n## 9. Tests & Projektstruktur\n\n```bash\npip install -r requirements-dev.txt\npytest -q            # 80 Tests: kostenlose Exchange-Feeds (Binance/Coinbase/DeFi Llama), Payment (USDC/USDT/Free Tier/Replay), Routing, Circuit-Breaker, Marge, Cache, Spread-Mathematik,\n                     #           API-Keys, Sliding-Window-Rate-Limiting, Usage-Tracking, /stats\nruff check .\n```\n\nTests benötigen **keinen** laufenden Redis (fakeredis) und **keinen** echten Facilitator (httpx MockTransport).\n\n```text\nagent_margin_router/\n├── app/\n│   ├── main.py                 # App-Factory, CORS, Lifespan (Redis connect/disconnect), Metriken\n│   ├── config.py               # pydantic-settings (.env)\n│   ├── routers/                # /extract-clean, /market-spread, /health, enterprise.py (/api-keys, /usage, /stats)\n│   ├── middleware/x402.py      # API-Key-Auflösung, Rate-Limits, 402-Challenge, Free Tier, Verify/Settle\n│   ├── services/\n│   │   ├── provider_router.py  # Scoring, Fallback, Circuit-Breaker, Mindestmarge, Tagesbudget\n│   │   ├── cache.py            # redis.asyncio Wrapper, Hit/Miss, Free-Tier-Zähler, Replay-Schutz\n│   │   ├── payment.py          # x402 Facilitator-Client (USDC + USDT)\n│   │   ├── api_keys.py         # API-Key-Erzeugung/-Auflösung/-Deaktivierung (Redis)\n│   │   ├── rate_limit.py       # Sliding-Window-Limiter (Redis ZSET), X-RateLimit-Header\n│   │   ├── usage.py            # Usage-Tracking pro Wallet/Key + globale Stats\n│   │   └── providers/          # base.py, scraping_provider.py, defi_provider.py\n│   ├── models/                 # Pydantic v2 Request/Response/Provider-Modelle\n│   └── utils/logging.py        # JSON-Logs mit Secret-Scrubbing\n├── tests/\n├── docker-compose.yml          # app + redis (+ cloudflared via --profile tunnel)\n├── Dockerfile                  # Multi-Stage, non-root, Healthcheck\n├── .env.example\n├── requirements.txt / requirements-dev.txt / pyproject.toml\n└── README.md\n```\n\n---\n\n**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.\n",
  "bytes": 30139,
  "sha": "b06e3a9fa23d399ef822171fba2a758506de599fb4953481e8e04eb8a5950185",
  "repo_slug": "agentmarginrouter/agentmarginrouter",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_agentmarginrouter_agent_margin_ec076f63/readme"
}