{"id":49914222,"url":"https://github.com/anoni-net/onionoo-fastapi","last_synced_at":"2026-05-16T15:00:57.687Z","repository":{"id":335732691,"uuid":"1146849764","full_name":"anoni-net/onionoo-fastapi","owner":"anoni-net","description":"Semantic/OpenAPI proxy for the Tor Metrics Onionoo API, built with FastAPI for easier integration and automated analysis.","archived":false,"fork":false,"pushed_at":"2026-05-15T05:20:33.000Z","size":292,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-15T07:33:23.279Z","etag":null,"topics":["agentic-ai","ai-agents","data-analysis","fastapi","network-metrics","observability","onionoo","openapi","privacy","pydantic","python","semantic-apis","tor","tor-metrics"],"latest_commit_sha":null,"homepage":"https://onionoo.anoni.net/docs","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/anoni-net.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-31T19:28:00.000Z","updated_at":"2026-02-14T07:58:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/anoni-net/onionoo-fastapi","commit_stats":null,"previous_names":["anoni-net/onionoo-fastapi"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/anoni-net/onionoo-fastapi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anoni-net%2Fonionoo-fastapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anoni-net%2Fonionoo-fastapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anoni-net%2Fonionoo-fastapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anoni-net%2Fonionoo-fastapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/anoni-net","download_url":"https://codeload.github.com/anoni-net/onionoo-fastapi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anoni-net%2Fonionoo-fastapi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33107564,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-16T04:41:52.686Z","status":"ssl_error","status_checked_at":"2026-05-16T04:41:52.009Z","response_time":115,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agentic-ai","ai-agents","data-analysis","fastapi","network-metrics","observability","onionoo","openapi","privacy","pydantic","python","semantic-apis","tor","tor-metrics"],"created_at":"2026-05-16T15:00:45.268Z","updated_at":"2026-05-16T15:00:57.673Z","avatar_url":"https://github.com/anoni-net.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# onionoo-fastapi\n\nFastAPI-based **semantic/OpenAPI proxy** for the Tor **Onionoo** API.\n\n- GitHub: \u003chttps://github.com/anoni-net/onionoo-fastapi\u003e\n- Upstream data source: \u003chttps://onionoo.torproject.org\u003e\n- This service **does not store Onionoo data**, it only forwards requests and transforms responses.\n- Primary motivation: Onionoo has a solid spec, but **no OpenAPI**; this service provides a friendly schema **for tooling/AI agents**.\n\nReference spec: [Tor Metrics – Onionoo](https://metrics.torproject.org/onionoo.html)\n\n## Hosted instance\n\n- Service: `https://onionoo.anoni.net`\n- Swagger UI: `https://onionoo.anoni.net/docs`\n\n## Releases\n\nTagged releases (`vX.Y.Z`) trigger two GitHub Actions workflows:\n\n- `.github/workflows/release.yml` — builds the wheel/sdist and publishes to\n  PyPI via Trusted Publishing. Register this workflow as a trusted publisher\n  on the `onionoo-fastapi` PyPI project once before the first tag.\n- `.github/workflows/docker.yml` — builds a multi-arch image and pushes to\n  `ghcr.io/\u003cowner\u003e/onionoo-fastapi`. Uses the default `GITHUB_TOKEN`.\n\nCut a release with:\n\n```bash\ngit tag -a v0.2.0 -m \"Release 0.2.0\"\ngit push origin v0.2.0\n```\n\n## License\n\nMIT. See `LICENSE`.\n\n## Requirements\n\n- Python 3.11+\n- [`uv`](https://docs.astral.sh/uv/)\n\n## Install\n\n```bash\ngit clone https://github.com/anoni-net/onionoo-fastapi\ncd onionoo-fastapi\nuv sync\n```\n\nOr if you already have the source:\n\n```bash\ncd onionoo-fastapi\nuv sync\n```\n\n## Run\n\n```bash\nfastapi run app.main:app --reload --host 0.0.0.0 --port 8000\n```\n**Note:** `fastapi run` requires FastAPI version 0.110.0 or newer.\n\nOpenAPI docs:\n\n- Swagger UI: `http://localhost:8000/docs`\n- OpenAPI JSON: `http://localhost:8000/openapi.json`\n\n## Test\n\n```bash\nuv sync --extra dev\nuv run pytest\n```\n\n## Docker\n\nBuild and run with Docker Compose:\n\n```bash\ndocker compose up -d --build\n```\n\nIf port 8000 is already in use, override host port (example: 8001):\n\n```bash\nHOST_PORT=8001 docker compose up -d --build\n```\n\nStop:\n\n```bash\ndocker compose down\n```\n\nConfiguration via environment variables (example):\n\n```bash\nONIONOO_BASE_URL=https://onionoo.torproject.org HOST_PORT=8001 docker compose up -d --build\n```\n\n## API\n\nThis service exposes semantic endpoints under `/v1/*`:\n\n- `GET /v1/summary`\n- `GET /v1/details`\n- `GET /v1/bandwidth`\n- `GET /v1/weights`\n- `GET /v1/clients`\n- `GET /v1/uptime`\n\nAggregate (server-side group-by, sorted by relay count):\n\n- `GET /v1/aggregate/countries` — buckets by two-letter country code\n- `GET /v1/aggregate/as` — buckets by autonomous system number\n- `GET /v1/aggregate/flags` — buckets by directory-authority flag (a relay can fall into multiple flag buckets)\n\nPlus:\n\n- `GET /healthz` — static liveness\n- `GET /healthz/ready` — verifies upstream reachability (cached)\n- `GET /metrics` — Prometheus format\n\n### Example requests\n\n```bash\n# Summary (semantic keys; upstream short keys are transformed)\ncurl -s 'http://localhost:8000/v1/summary?limit=1' | jq .\n\n# Details (supports Onionoo query parameters + details-only `fields`)\ncurl -s 'http://localhost:8000/v1/details?limit=1\u0026search=moria\u0026fields=nickname,fingerprint' | jq .\n\n# Bandwidth\ncurl -s 'http://localhost:8000/v1/bandwidth?limit=1\u0026search=moria' | jq .\n\n# Weights (relays only)\ncurl -s 'http://localhost:8000/v1/weights?limit=1\u0026search=moria' | jq .\n\n# Clients (bridges only)\ncurl -s 'http://localhost:8000/v1/clients?limit=1' | jq .\n\n# Uptime\ncurl -s 'http://localhost:8000/v1/uptime?limit=1\u0026search=moria' | jq .\n```\n\n### Semantic field mapping notes\n\n- `/v1/summary` transforms Onionoo short keys:\n  - relay: `n,f,a,r` → `nickname,fingerprint,addresses,running`\n  - bridge: `n,h,r` → `nickname,hashed_fingerprint,running`\n- For some bridge documents (`/bandwidth`, `/clients`, `/uptime`), Onionoo uses the key name `fingerprint` even though the value is a **hashed fingerprint**; this API exposes that as `hashed_fingerprint`.\n\n### Caching / 304 behavior\n\nIf the client includes `If-Modified-Since`, it will be forwarded upstream. If Onionoo replies with `304`, this service will reply `304` too.\n\n### Configuration\n\nUpstream / cache:\n\n- `ONIONOO_BASE_URL` (default: `https://onionoo.torproject.org`)\n- `ONIONOO_TIMEOUT_SECONDS` (default: `30`)\n- `DEFAULT_LIMIT` (default: `100`)\n- `MAX_LIMIT` (default: `200`)\n- `USER_AGENT`\n- `CACHE_MAXSIZE` (default: `1024`)\n- `CACHE_DEFAULT_TTL_SECONDS` (default: `300`)\n- `UPSTREAM_RETRY_ATTEMPTS` (default: `2`)\n\nObservability / production hardening:\n\n- `LOG_LEVEL` (default: `INFO`)\n- `LOG_FORMAT` (`json` or `console`, default `json`)\n- `METRICS_ENABLED` (default: `true`) — exposes `/metrics` in Prometheus format\n- `CORS_ALLOWED_ORIGINS` (default: empty, CORS disabled). Example: `[\"https://example.com\"]`\n- `RATE_LIMIT_ENABLED` (default: `false`)\n- `RATE_LIMIT_PER_MINUTE` (default: `120`)\n- `HEALTHZ_READY_CACHE_SECONDS` (default: `30`)\n\n### Resource sizing\n\nA single-worker container (the default `uvicorn` CMD in the Dockerfile) measured on Alpine 3.23 / Python 3.14 / aarch64 against the real Onionoo upstream:\n\n| Phase | RSS | Notes |\n|---|---:|---|\n| **Idle** (just after start) | ~75 MiB | Python + FastAPI + Pydantic + httpx + fastapi-mcp + structlog + Prometheus instrumentator loaded; cache empty. |\n| **Typical agent traffic** | ~90 MiB | After ~15 mixed `/v1/*` calls (details + aggregates), only a handful of distinct upstream payloads cached. |\n| **Cache near saturation** | ~180 MiB | After 200 distinct `/v1/details` queries with `fields=` projection; cache holds ~200 entries. |\n\nFrom these measurements, each cached entry costs **~0.5 MiB on average** when callers use the `fields=` projection. With the default `CACHE_MAXSIZE=1024` that yields a **~500 MiB upper bound** under realistic agent traffic.\n\nIf you expect callers to hit `/v1/details` **without** `fields=`, a single response can be several MiB (Onionoo returns ~10k full relay objects). A fully saturated cache of unfiltered details would then sit in the **1–5 GiB** range — bound it by tuning `CACHE_MAXSIZE` down.\n\nSuggested memory limits for `docker run --memory` / Kubernetes requests:\n\n| Deployment shape | Memory request | Memory limit |\n|---|---:|---:|\n| Personal / single-agent test | 128 MiB | 256 MiB |\n| Hosted instance, mostly cached requests | 256 MiB | 512 MiB |\n| Public instance, agents may issue unfiltered `/details` | 512 MiB | 1–2 GiB |\n\nCPU is light — a single worker handles 10s of QPS comfortably; scale with replicas if you need more throughput. (`uvicorn ... --workers N` is also an option, but each worker keeps its own in-memory cache; horizontal scaling via separate containers is usually a better fit.)\n\n### Health checks\n\n- `GET /healthz` — static liveness probe, never hits upstream.\n- `GET /healthz/ready` — pings Onionoo (`summary?limit=1`); 200 when reachable, 503 otherwise. Result is cached for `HEALTHZ_READY_CACHE_SECONDS`.\n\n### Request tracing\n\nEvery request is assigned an `X-Request-ID`. Clients may supply one to correlate across systems; the same value is echoed back on the response and bound into every log record produced during the request.\n\n### Metrics\n\n`/metrics` exposes Prometheus-format counters / histograms, including:\n\n- `onionoo_cache_hits_total`, `onionoo_cache_misses_total`\n- `onionoo_upstream_seconds{method=...}` (histogram)\n- `onionoo_upstream_errors_total{method=..., status=...}`\n- Standard `http_request_duration_seconds` from the FastAPI instrumentator\n\n### Raw passthrough\n\nFor large payloads (`/v1/details`), pass `?raw=true` to skip Pydantic re-validation and forward the upstream JSON verbatim. Trade-off: raw mode does **not** apply semantic key remapping (e.g. on `/v1/summary` you'll see `n,f,a,r` rather than `nickname,fingerprint,addresses,running`) and **no `_meta` block is injected**.\n\n### Response metadata (`_meta`)\n\nNon-raw responses on `/v1/*` include a proxy-injected `_meta` block at the top of the envelope:\n\n```json\n{\n  \"_meta\": {\n    \"cache_age_seconds\": 12.345,\n    \"upstream_last_modified\": \"Thu, 15 May 2026 12:00:00 GMT\"\n  },\n  \"version\": \"9.0\",\n  ...\n}\n```\n\n`cache_age_seconds = 0.0` means the response was just fetched from Onionoo. A non-zero value means the proxy served it from its in-memory cache.\n\n### Trimming payloads with `fields=`\n\nAll `/v1/*` endpoints accept `?fields=a,b,c`. On `/summary` and `/details` Onionoo applies the projection at the upstream level; on history endpoints (bandwidth, weights, clients, uptime) Onionoo applies it where supported. Using it on large queries can shrink LLM input by an order of magnitude.\n\n## Use as an MCP server\n\nThis project ships an [MCP](https://modelcontextprotocol.io) server with two\ntransports — pick whichever fits your client.\n\n### Tools\n\n**Task-oriented (recommended for agents)**\n\n- `find_relay(query)` — free-form lookup; auto-detects fingerprint, AS, IP, or nickname\n- `get_relay_health(fingerprint)` — composite snapshot (details + uptime + bandwidth)\n- `top_relays_by_bandwidth(country?, flag?, limit)` — top-N by consensus weight\n- `compare_relays(fingerprints)` — parallel side-by-side details\n- `country_summary(country)` — running relay count, total bandwidth, flag distribution\n\n**Low-level pass-through (raw Onionoo endpoints)**\n\n- `onionoo_summary`, `onionoo_details`, `onionoo_bandwidth`, `onionoo_weights`,\n  `onionoo_clients`, `onionoo_uptime` — each takes a `params` dict matching the\n  [Onionoo query spec](https://metrics.torproject.org/onionoo.html).\n\n**Aggregates**\n\n- `aggregate_relays(group_by=\"country\"|\"as\"|\"flag\", running=True, top=N)` — server-side group-by, sorted by relay count.\n\n\u003e Streamable HTTP `/mcp` exposes the six low-level endpoints (`get_summary` …\n\u003e `get_uptime`) plus the three aggregate endpoints (`aggregate_countries`,\n\u003e `aggregate_as`, `aggregate_flags`). The task-oriented tools and the unified\n\u003e `aggregate_relays` live in the stdio server. Both transports can run side by side.\n\n### Streamable HTTP transport (recommended for hosted use)\n\nRun the FastAPI app — `/mcp` is mounted automatically.\n\nInspect with MCP Inspector:\n\n```bash\nnpx @modelcontextprotocol/inspector\n# Transport: Streamable HTTP\n# URL: http://localhost:8000/mcp\n```\n\nClaude Desktop / Cursor:\n\n```json\n{\n  \"mcpServers\": {\n    \"onionoo\": {\n      \"type\": \"http\",\n      \"url\": \"https://onionoo.anoni.net/mcp\"\n    }\n  }\n}\n```\n\n### stdio transport (recommended for local agents)\n\n`uv sync` installs an `onionoo-mcp` console script:\n\n```bash\nonionoo-mcp\n```\n\nClaude Desktop / Cursor:\n\n```json\n{\n  \"mcpServers\": {\n    \"onionoo\": {\n      \"command\": \"uvx\",\n      \"args\": [\"--from\", \"/path/to/onionoo-fastapi\", \"onionoo-mcp\"]\n    }\n  }\n}\n```\n\nOr, if the repo is checked out and you have `uv`:\n\n```json\n{\n  \"mcpServers\": {\n    \"onionoo\": {\n      \"command\": \"uv\",\n      \"args\": [\"--directory\", \"/path/to/onionoo-fastapi\", \"run\", \"onionoo-mcp\"]\n    }\n  }\n}\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanoni-net%2Fonionoo-fastapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanoni-net%2Fonionoo-fastapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanoni-net%2Fonionoo-fastapi/lists"}