Back to the catalog

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

[![Live API](https://img.shields.io/badge/API-live-22c55e)](https://agent-margin-router-production.up.railway.app/health)
[![Docs](https://img.shields.io/badge/docs-landing%20page-06b6d4)](https://agentmarginrouter.com)
[![x402](https://img.shields.io/badge/payments-x402%20%7C%20USDC%20%2B%20USDT%20on%20Base-8b5cf6)](https://www.x402.org)
[![Python](https://img.shields.io/badge/python-3.11%2B-3776ab)](#)
[![Tests](https://img.shields.io/badge/tests-80%20passing-22c55e)](#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.

More