zora-analytics

zora-analytics

Enables querying live Zora token data and analytics methodology through SQL and document retrieval tools, providing guarded access to on-chain holder, transfer, and token information.

Category
访问服务器

README

On-Chain SQL + RAG Analytics Agent

CI

Ask questions in plain English about live Zora token data or the project's analytics methodology. The agent routes quantitative questions to guarded SQL and conceptual questions to hybrid document retrieval, then returns the answer together with SQL rows and/or cited chunks.

The model provider is swappable: Anthropic and OpenAI API adapters are available, while claude_code uses an authenticated Claude Code Pro/Max subscription without API-token billing.

Tests: 107 tests currently pass, including the SQL attack suite, circuit/cache behavior, MCP protocol handshake, retrieval provenance, and eval scorers.

What this demonstrates

Capability Implementation
Bounded agentic tool use run_sql + search_docs, with at most two tool executions in agent/service.py
Defense-in-depth SQL safety Fail-closed SQLGlot AST policy, allowlisted tables, row/timeout caps, and a SELECT-only role
Hybrid RAG / vector database pgvector cosine search + PostgreSQL FTS + Reciprocal Rank Fusion in retrieval/
MCP integration Official MCP Python SDK, FastMCP, stdio + Streamable HTTP in mcp_server/
OpenTelemetry observability OpenTelemetry-native Langfuse spans, structured stdout, and query_logs
Resilience and cost control Transient retries, provider circuit breaker, normalized answer cache, stale fallback
Measured quality A 44-case adversarial SQL harness plus a real 28-case retrieval ablation in eval/
Provider independence Anthropic, OpenAI, or local Claude Code subscription through the same LLMClient protocol

Architecture

flowchart LR
    Z[Zora explorer] --> I[Scheduled indexer]
    I --> P[(Postgres + pgvector)]
    U[Streamlit / API client] --> A[FastAPI /ask]
    A --> G[Bounded agent]
    G -->|run_sql| S[AST SQL guard]
    S -->|read-only transaction| P
    G -->|search_docs| R[Dense + FTS + RRF]
    R -->|guarded SELECTs| S
    P --> G
    G --> U
    C[MCP client] --> M[FastMCP stdio / HTTP]
    M --> S
    T[OpenTelemetry spans] --> L[Langfuse optional]
    G --> T
    M --> T

Quickstart

Prerequisites: Docker Desktop and Python 3.11+.

cp .env.example .env
python3 -m venv .venv
.venv/bin/pip install -e '.[all,dev]'
docker compose up -d postgres

New database volumes initialize the schema and roles automatically. To upgrade an existing project volume after pulling this version, apply the additive schema once:

docker compose exec -T postgres \
  psql -v ON_ERROR_STOP=1 -U postgres -d zora_analytics -f /opt/zora/schema.sql

Index the bundled Markdown corpus. The first command allows one download of the 67 MB quantized BGE ONNX model; subsequent embedding runs work from the local cache without an API key:

.venv/bin/zora-retrieval-index --allow-model-download

Run with a Claude subscription

Confirm that the host CLI is authenticated:

claude auth status

Set these values in .env, then run the application on the host so it can invoke your logged-in claude binary:

LLM_PROVIDER=claude_code
CLAUDE_CODE_MODEL=sonnet
CLAUDE_CODE_EFFORT_LEVEL=low

The model alias currently selects the latest Sonnet, while the explicit low-effort setting keeps routine Text-to-SQL requests from inheriting a higher global Claude Code effort. To favor maximum speed over SQL reliability, set CLAUDE_CODE_MODEL=haiku; Haiku does not use an effort level.

Run the API:

.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000

In a second terminal:

API_BASE_URL=http://127.0.0.1:8000 \
  .venv/bin/streamlit run ui/streamlit_app.py --server.address 127.0.0.1 --server.port 8501

Open the API docs or the chat UI. Claude Code subscription usage reports cost_usd: null; it does not pretend subscription calls have API-token cost.

Run with an API provider

Set either ANTHROPIC_API_KEY or OPENAI_API_KEY and the matching LLM_PROVIDER. The fully containerized path is then. Compose mounts the host .cache/fastembed directory read-only, so the model cached during the indexing step is reused without another download:

docker compose up --build

Health and direct query examples:

curl http://127.0.0.1:8000/health
curl -X POST http://127.0.0.1:8000/ask \
  -H 'Content-Type: application/json' \
  -d '{"question":"Who are the top 10 current holders?"}'

Guardrails

Every model- or MCP-supplied query goes through one SQLGuard and one SQLExecutor. The guard:

  • accepts exactly one read-only SELECT;
  • allowlists tokens, holders, transfers, and retrieval embeddings;
  • blocks writes, DDL, system schemas, cross-database references, locks, and dangerous functions;
  • preserves pgvector's <=> cosine operator only on embeddings.embedding;
  • injects or clamps LIMIT, while the executor sets TRANSACTION READ ONLY and a statement timeout.

The database reader role is a separate safety boundary. A parser regression still cannot turn the reader credential into a writer.

Resilience and cache

Provider calls receive transient-only exponential-backoff retries. After four consecutive failures by default, the process-wide provider/model circuit opens for 30 seconds. These values are controlled by LLM_PROVIDER_RETRY_ATTEMPTS, LLM_CIRCUIT_FAILURE_THRESHOLD, and LLM_CIRCUIT_RESET_SECONDS.

Successful responses are cached in answer_cache by a hash of:

  1. case-folded, punctuation/whitespace-normalized question;
  2. tracked token address;
  3. current introspected schema hash.

A fresh hit skips the model. If the provider is unavailable, an expired last-good response can be served explicitly as stale. Provider and guard failures return HTTP 200 with a machine-readable contract instead of a 5xx:

{
  "status": "degraded",
  "answer": "I couldn't answer that confidently.",
  "reason": "provider_unavailable",
  "last_error": "...",
  "served_from_cache": false
}

GET /health includes answer-cache lookups, hits, stale hits, and process-local hit rate.

Observability

observability/tracing.py is a deliberately thin span API. One trace covers every /ask or MCP call, with nested llm.generate, tool.run_sql, tool.search_docs, guard.validate, db.execute, and retrieval.* spans. Provider/model, tokens, nullable cost, latency, retries, guard decision, and row counts use the same values returned by the API and written to query_logs.

Langfuse is optional. If any of LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, or LANGFUSE_HOST is missing, tracing runs as a no-op and cannot break a request. For Langfuse Cloud, create a project, put its three values in .env, and restart the API.

For local Langfuse v3:

docker compose --profile observability up -d

Open localhost:3000, create the first project/key pair, place the keys in .env, and set LANGFUSE_HOST=http://localhost:3000 for a host-run API. For the Compose API, set LANGFUSE_DOCKER_HOST=http://langfuse-web:3000. The optional LANGFUSE_INIT_* variables perform the same organization/project/user setup headlessly on first startup. Change all LANGFUSE_* local secrets before exposing that profile. OTEL_SERVICE_NAME defaults to zora-analytics-agent so new traces have a stable service identity instead of an anonymous OpenTelemetry resource.

A local smoke run on 2026-07-18 sent a real MCP describe_schema call through this instrumentation: Langfuse stored one mcp.call trace with a nested db.execute span. The local project uses a non-personal demo identity; no developer account information is attached to the trace.

Langfuse trace showing an MCP call with nested database execution

Langfuse's current Python SDK is OpenTelemetry-native, so the instrumentation is not coupled to a custom agent framework and can coexist with another OTel backend.

MCP server

The MCP server is a parallel interface to the same guarded executor. It exposes:

  • run_sql(query) — structured rows or a structured guard rejection;
  • describe_schema() — the runtime-introspected analytics schema;
  • data_freshness() — token watermark plus latest sync run;
  • top_holders(limit=10) — a curated query, still passed through the guard.

Claude Desktop claude_desktop_config.json:

{
  "mcpServers": {
    "zora-analytics": {
      "command": "/absolute/path/to/zoraHoldersAI/.venv/bin/zora-mcp",
      "args": ["--transport", "stdio"],
      "env": {
        "DATABASE_URL": "postgresql://zora_app:zora_app@localhost:55432/zora_analytics",
        "READ_ONLY_DATABASE_URL": "postgresql://zora_reader:zora_reader@localhost:55432/zora_analytics"
      }
    }
  }
}

Replace only the absolute repository path and, if necessary, the mapped PostgreSQL port. Any MCP-compatible client can use the same stdio command. Streamable HTTP is one line:

.venv/bin/zora-mcp --transport streamable-http --host 127.0.0.1 --port 8001

Clients connect to http://127.0.0.1:8001/mcp.

Hybrid retrieval

The curated corpus in retrieval/corpus/ covers protocol scope, on-chain terminology, timestamp semantics, safety, and system methodology. Heading-aware chunks keep stable provenance such as methodology#meaning-of-first-seen.

For a query, the retriever runs two guarded SELECTs over the same PostgreSQL instance:

  1. BGE-small dense cosine search through pgvector;
  2. English full-text search through a generated tsvector + GIN index;
  3. deterministic RRF fusion; optionally, a local FastEmbed cross-encoder reranker.

No paid embedding API is required. EMBEDDINGS_PROVIDER=openai remains an explicit option, while fastembed is the default. With FASTEMBED_LOCAL_FILES_ONLY=true, a cached BGE model is used without network; if it is not cached, a deterministic local hash embedding keeps the feature fail-soft and is clearly labeled as a fallback rather than BGE benchmark output.

Retrieval eval results

The table below came from a real local run on 2026-07-18 against 28 golden questions and 33 indexed chunks. It was generated with python -m eval.run_retrieval --allow-model-download --judge; no values were hand-waved.

<!-- RETRIEVAL_RESULTS_START --> Real retrieval run using BAAI/bge-small-en-v1.5; ranking cutoff k=5.

Configuration Precision@5 Recall@5 MRR Groundedness Status
Dense only 22.14% 89.29% 0.8810 96.43% completed
Dense + FTS (RRF) 22.86% 92.86% 0.9315 92.86% completed
Hybrid + reranker 23.57% 94.64% 0.9643 96.43% completed

An unavailable reranker is reported explicitly and is never relabeled as a reranked run. <!-- RETRIEVAL_RESULTS_END -->

Retrieval ablation

Run it again with:

.venv/bin/zora-retrieval-eval --allow-model-download

Add --judge to generate evidence-only answers and reuse the provider-neutral groundedness judge. The default gate fails if hybrid MRR drops below dense-only MRR. Raw per-question results live in eval/results/retrieval-*.json.

SQL eval results

<!-- EVAL_RESULTS_START -->

Metric Value
Execution accuracy 100.00%
Valid-SQL rate 100.00%
Clarification accuracy 100.00%
Answer groundedness 88.64%
Mean latency 7.01 s
p95 latency 10.60 s
Mean cost / query n/a

Cases: 44 total / 37 executable.

<details> <summary>Per-case results</summary>

ID Difficulty Check Correct Valid SQL Latency Cost
q001 easy result_set_match 6.77 s n/a
q002 easy numeric_match 5.91 s n/a
q003 easy result_set_match 7.32 s n/a
q004 easy numeric_match 6.73 s n/a
q005 easy numeric_match 5.87 s n/a
q006 easy numeric_match 5.59 s n/a
q007 easy numeric_match 5.01 s n/a
q008 easy numeric_match 5.67 s n/a
q009 easy numeric_match 5.23 s n/a
q010 easy result_set_match 7.92 s n/a
q011 easy numeric_match 7.90 s n/a
q012 easy result_set_match 6.40 s n/a
q013 easy numeric_match 5.14 s n/a
q014 medium numeric_match 7.70 s n/a
q015 medium result_set_match 6.29 s n/a
q016 hard numeric_match 6.74 s n/a
q017 medium numeric_match 7.48 s n/a
q018 easy numeric_match 5.26 s n/a
q019 medium result_set_match 9.64 s n/a
q020 medium numeric_match 5.86 s n/a
q021 medium numeric_match 7.33 s n/a
q022 medium numeric_match 5.89 s n/a
q023 medium numeric_match 10.15 s n/a
q024 medium numeric_match 10.60 s n/a
q025 medium result_set_match 6.52 s n/a
q026 medium result_set_match 8.19 s n/a
q027 medium numeric_match 5.85 s n/a
q028 medium numeric_match 7.21 s n/a
q029 hard result_set_match 8.62 s n/a
q030 hard result_set_match 9.81 s n/a
q031 hard numeric_match 9.40 s n/a
q032 hard numeric_match 7.88 s n/a
q033 medium result_set_match 5.90 s n/a
q034 medium numeric_match 6.63 s n/a
q035 ambiguous clarification n/a 4.12 s n/a
q036 ambiguous clarification n/a 3.46 s n/a
q037 ambiguous clarification n/a 3.60 s n/a
q038 ambiguous clarification n/a 4.27 s n/a
q039 hard result_set_match 12.75 s n/a
q040 hard numeric_match 10.36 s n/a
q041 hard result_set_match 15.44 s n/a
q042 unsupported clarification n/a 4.46 s n/a
q043 unsupported clarification n/a 6.10 s n/a
q044 ambiguous clarification n/a 3.38 s n/a

</details> <!-- EVAL_RESULTS_END -->

This is a bounded regression benchmark over one indexed token snapshot, not a claim of universal Text-to-SQL accuracy. The set deliberately includes multi-relation joins, unavailable-data rejections, and ambiguous questions rather than only straightforward aggregate queries. The published run used the sonnet alias at high effort; the application now defaults to low effort for subsequent interactive queries. Changing inference settings does not alter historical results.

Evaluation-engineering highlights:

  • Execution accuracy is based on database result comparison, not generated-SQL string similarity.
  • A broken or timed-out reference query invalidates the run instead of counting as an agent error.
  • Result-set matching remains strict, while numeric comparisons use narrow rounding tolerance.
  • The groundedness judge measures faithfulness separately from SQL correctness; calibration and answer-grounding work raised the measured score from 26.32% to 88.64%.
  • The independent retrieval ablation improved Recall@5 from 89.29% to 94.64% (+5.35 percentage points), with sparse retrieval changing 26 of 28 rankings.

SQL execution accuracy compares each generated query's actual result set with a hand-written reference query. It is order- and alias-insensitive and tolerance-aware for numeric answers. Clarification cases accept either a direct clarification or a cited, row-free, SQL-free rejection; an unsupported metric answered with guessed SQL still fails.

Repository structure

.github/         CI for lint, formatting, tests, and both Compose configurations
agent/           bounded tool loop, cache, circuit breaker, prompts
app/             FastAPI: /ask, /admin/sync, /health
db/              Postgres/pgvector schema, roles, schema introspection
eval/            SQL golden eval + retrieval golden set/ablation
indexer/         scheduled Zora holder/transfer synchronization
llm/             Anthropic, OpenAI, and Claude Code adapters
mcp_server/      official FastMCP server and CLI
observability/   Langfuse/Otel spans + structured/query-table logging
retrieval/       corpus, chunking, embeddings, indexing, dense/FTS/RRF
sql_guard/       fail-closed AST guard + read-only executor
ui/              Streamlit chat
tests/           package-mirrored unit and protocol tests

Development

.venv/bin/pip install -e '.[all,dev]'
.venv/bin/pytest -q
.venv/bin/ruff check .
docker compose config --quiet

The optional dependency groups are observability, mcp, and retrieval; all is used by the runtime Docker image. No LangChain or LlamaIndex dependency is used.

License

MIT. See LICENSE.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选