diff --git a/apps/api/src/hackproof/db.py b/apps/api/src/hackproof/db.py
index f72d7b5..5370e23 100644
--- a/apps/api/src/hackproof/db.py
+++ b/apps/api/src/hackproof/db.py
@@ -88,9 +88,11 @@
total_commits INTEGER,
stellar_added_in_window BOOLEAN,
scanned_at TIMESTAMPTZ,
- declared_track TEXT
+ declared_track TEXT,
+ tooling JSONB NOT NULL DEFAULT '{}'
);
ALTER TABLE repos ADD COLUMN IF NOT EXISTS declared_track TEXT;
+ALTER TABLE repos ADD COLUMN IF NOT EXISTS tooling JSONB NOT NULL DEFAULT '{}';
CREATE TABLE IF NOT EXISTS repo_contributors (
full_name TEXT NOT NULL,
diff --git a/apps/api/src/hackproof/github/dependencies.py b/apps/api/src/hackproof/github/dependencies.py
index 91ded2b..288b1cf 100644
--- a/apps/api/src/hackproof/github/dependencies.py
+++ b/apps/api/src/hackproof/github/dependencies.py
@@ -2,6 +2,7 @@
import re
import tomllib
from pathlib import PurePosixPath
+from typing import Any
from hackproof.github.models import DependencyFingerprint
@@ -16,6 +17,29 @@
REQUIREMENT = re.compile(r"^([A-Za-z0-9_.-]+)\s*(?:[=~<>!]=?\s*([\w.*-]+))?")
GO_REQUIRE = re.compile(r"^\s*(?:require\s+)?(\S+)\s+(v[\w.+-]+)", re.MULTILINE)
+OPENZEPPELIN_PACKAGES = {
+ "stellar-contracts",
+ "openzeppelin-stellar-contracts",
+ "openzeppelin_stellar_contracts",
+ "@openzeppelin/contracts",
+ "@openzeppelin/stellar-contracts",
+}
+
+WALLETS_KIT_PACKAGES = {
+ "@creit.tech/stellar-wallets-kit",
+ "stellar-wallets-kit",
+}
+
+FREIGHTER_PACKAGES = {
+ "@stellar/freighter-api",
+ "freighter-api",
+}
+
+PASSKEY_PACKAGES = {
+ "passkey-kit",
+ "@stellar/passkey-kit",
+}
+
def _cargo(text: str) -> dict[str, str | None]:
data = tomllib.loads(text)
@@ -82,3 +106,60 @@ def fingerprint(files: dict[str, str], wanted: dict[str, list[str]]) -> Dependen
if ecosystem not in result.ecosystems:
result.ecosystems.append(ecosystem)
return result
+
+
+def detect_openzeppelin(files: dict[str, str]) -> bool:
+ for path, text in files.items():
+ name = PurePosixPath(path).name
+ if name not in PARSERS:
+ continue
+ try:
+ declared = PARSERS[name](text)
+ if any(pkg in declared for pkg in OPENZEPPELIN_PACKAGES):
+ return True
+ except Exception:
+ continue
+ return False
+
+
+def detect_framework(files: dict[str, str]) -> str:
+ for path, text in files.items():
+ if PurePosixPath(path).name == "package.json":
+ try:
+ data = json.loads(text)
+ deps: dict[str, Any] = {}
+ for section in ("dependencies", "devDependencies"):
+ deps.update(data.get(section, {}))
+ if "next" in deps:
+ return "Next.js"
+ if "vite" in deps:
+ return "Vite"
+ except Exception:
+ continue
+ return "Other"
+
+
+def detect_ai_tooling(paths: list[str]) -> list[str]:
+ found = set()
+ for path in paths:
+ normalized = "/" + path.strip("/").lower()
+ parts = PurePosixPath(path).parts
+ name = parts[-1].lower() if parts else ""
+ if name == "claude.md":
+ found.add("CLAUDE.md")
+ elif name == "agents.md":
+ found.add("AGENTS.md")
+ elif name == ".cursorrules" or ".cursor/rules" in normalized or "cursorrules" in name:
+ found.add("Cursor rules")
+ elif "copilot-instructions" in normalized:
+ found.add("Copilot instructions")
+ return sorted(found)
+
+
+def parse_major_version(spec: str | None) -> str:
+ if not spec:
+ return "Unknown"
+ m = re.search(r"(\d+)", spec)
+ if m:
+ return f"v{m.group(1)}"
+ return "Unknown"
diff --git a/apps/api/src/hackproof/github/models.py b/apps/api/src/hackproof/github/models.py
index f5ec733..93b64a5 100644
--- a/apps/api/src/hackproof/github/models.py
+++ b/apps/api/src/hackproof/github/models.py
@@ -1,5 +1,5 @@
from datetime import datetime
-from typing import Literal
+from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
@@ -60,3 +60,4 @@ class RepoScan(BaseModel):
stellar_added_in_window: bool | None
integrations: list[Detection]
contributors: list[Contributor]
+ tooling: dict[str, Any] = Field(default_factory=dict)
diff --git a/apps/api/src/hackproof/github/scan.py b/apps/api/src/hackproof/github/scan.py
index 375ae74..70259a2 100644
--- a/apps/api/src/hackproof/github/scan.py
+++ b/apps/api/src/hackproof/github/scan.py
@@ -8,7 +8,13 @@
from hackproof.github.client import GithubClient
from hackproof.github.demo import scan_demo
-from hackproof.github.dependencies import MANIFESTS, fingerprint
+from hackproof.github.dependencies import (
+ MANIFESTS,
+ detect_ai_tooling,
+ detect_framework,
+ detect_openzeppelin,
+ fingerprint,
+)
from hackproof.github.files import fetch_texts
from hackproof.github.history import stellar_added_in_window
from hackproof.github.models import Candidate, Contributor, Ref, RepoScan
@@ -123,6 +129,12 @@ async def scan(self, repo: Candidate) -> RepoScan | None:
if dependencies.packages
else None
)
+ all_paths = [entry.get("path", "") for entry in tree.get("tree", [])]
+ tooling = {
+ "framework": detect_framework(files),
+ "ai": detect_ai_tooling(all_paths),
+ "openzeppelin": detect_openzeppelin(files),
+ }
return RepoScan(
full_name=repo.full_name,
declared_track=declared_track(files),
@@ -131,6 +143,7 @@ async def scan(self, repo: Candidate) -> RepoScan | None:
stellar_added_in_window=added,
integrations=detect(files, self._catalog, repo.owner),
contributors=contributors_from(people),
+ tooling=tooling,
)
@@ -178,11 +191,12 @@ async def store_scan(connection: asyncpg.Connection, scan: RepoScan) -> None:
await connection.execute(
"""
UPDATE repos SET dependencies = $2, stellar_added_in_window = $3,
- declared_track = $4, scanned_at = now()
+ declared_track = $4, tooling = $5, scanned_at = now()
WHERE full_name = $1
""",
scan.full_name,
scan.dependencies.model_dump(),
scan.stellar_added_in_window,
scan.declared_track,
+ scan.tooling,
)
diff --git a/apps/api/src/hackproof/hackathon.py b/apps/api/src/hackproof/hackathon.py
index a6ef8e3..74c5ccc 100644
--- a/apps/api/src/hackproof/hackathon.py
+++ b/apps/api/src/hackproof/hackathon.py
@@ -36,6 +36,7 @@ class Hackathon(BaseModel):
window: Window
awards: list[Award] = []
discovery: Discovery
+ hidden: list[str] = []
@lru_cache
@@ -46,3 +47,14 @@ def get_hackathon() -> Hackathon:
def places() -> dict[str, Place]:
return {award.project_id: award.place for award in get_hackathon().awards}
+
+
+def hidden_ids() -> list[str]:
+ raw = get_hackathon().hidden
+ ids = set(raw)
+ for item in raw:
+ if item.startswith("repo:"):
+ ids.add(item[5:])
+ elif not item.startswith("acct:"):
+ ids.add(f"repo:{item}")
+ return sorted(ids)
diff --git a/apps/api/src/hackproof/queries.py b/apps/api/src/hackproof/queries.py
index 9f9e1ab..c38ec51 100644
--- a/apps/api/src/hackproof/queries.py
+++ b/apps/api/src/hackproof/queries.py
@@ -1,8 +1,9 @@
+from collections import Counter
from datetime import datetime
from typing import Any, Literal
from hackproof.db import pool
-from hackproof.hackathon import get_hackathon, places
+from hackproof.hackathon import get_hackathon, hidden_ids, places
from hackproof.integrations.catalog import get_catalog
from hackproof.linking.methods import METHOD_RANK_SQL
@@ -256,3 +257,161 @@ async def fetch_integrations() -> list[dict[str, Any]]:
async def fetch_countries(limit: int = 12) -> list[dict[str, Any]]:
async with pool().acquire() as connection:
return [dict(row) for row in await connection.fetch(COUNTRIES_SQL, limit)]
+
+TOOLING_SQL = """
+SELECT p.project_id, p.full_name, r.dependencies, COALESCE(r.tooling, '{}'::jsonb) AS tooling,
+ ARRAY(SELECT i.partner FROM repo_integrations i
+ WHERE i.full_name = p.full_name) AS integrations
+FROM projects p
+JOIN repos r ON r.full_name = p.full_name
+WHERE p.tier IN ('verified', 'likely')
+ AND ($1::text[] IS NULL OR NOT (p.project_id = ANY($1) OR p.full_name = ANY($1)))
+"""
+
+
+async def fetch_tooling() -> dict[str, Any]:
+ from hackproof.github.dependencies import parse_major_version
+
+ hidden = hidden_ids() or None
+ async with pool().acquire() as connection:
+ rows = await connection.fetch(TOOLING_SQL, hidden)
+
+ total = len(rows)
+
+ oz_count = 0
+ wallets_count = 0
+ freighter_count = 0
+ passkey_count = 0
+
+ soroban_versions: Counter[str] = Counter()
+ stellar_versions: Counter[str] = Counter()
+ framework_counts: Counter[str] = Counter()
+ ai_counts: Counter[str] = Counter()
+
+ for row in rows:
+ deps = row["dependencies"] or {}
+ if isinstance(deps, str):
+ import json
+ deps = json.loads(deps)
+ packages = deps.get("packages", {}) or {}
+ tooling = row["tooling"] or {}
+ if isinstance(tooling, str):
+ import json
+ tooling = json.loads(tooling)
+ integrations = set(row["integrations"] or [])
+
+ # 1. OpenZeppelin Stellar Contracts
+ has_oz = (
+ tooling.get("openzeppelin")
+ or any(
+ p in packages
+ for p in (
+ "stellar-contracts",
+ "openzeppelin-stellar-contracts",
+ "openzeppelin_stellar_contracts",
+ "@openzeppelin/contracts",
+ "@openzeppelin/stellar-contracts",
+ )
+ )
+ or "openzeppelin" in integrations
+ )
+ if has_oz:
+ oz_count += 1
+
+ # 2. Stellar Wallets Kit
+ has_wallets = (
+ "@creit.tech/stellar-wallets-kit" in packages
+ or "stellar-wallets-kit" in packages
+ or "stellar-wallets-kit" in integrations
+ )
+ if has_wallets:
+ wallets_count += 1
+
+ # 3. Freighter API
+ has_freighter = (
+ "@stellar/freighter-api" in packages
+ or "freighter" in integrations
+ or "freighter-api" in packages
+ )
+ if has_freighter:
+ freighter_count += 1
+
+ # 4. Passkey Kit
+ has_passkey = (
+ "passkey-kit" in packages
+ or "@stellar/passkey-kit" in packages
+ or "passkey" in integrations
+ )
+ if has_passkey:
+ passkey_count += 1
+
+ # 5. soroban-sdk major versions
+ if "soroban-sdk" in packages:
+ ver = parse_major_version(packages["soroban-sdk"])
+ soroban_versions[ver] += 1
+
+ # 6. stellar-sdk JS versions
+ sdk_spec = packages.get("@stellar/stellar-sdk") or packages.get("stellar-sdk")
+ if sdk_spec:
+ ver = parse_major_version(sdk_spec)
+ stellar_versions[ver] += 1
+
+ # 7. Frameworks
+ fw = tooling.get("framework")
+ if not fw or fw == "Other":
+ if "next" in packages:
+ fw = "Next.js"
+ elif "vite" in packages:
+ fw = "Vite"
+ else:
+ fw = "Other"
+ framework_counts[fw] += 1
+
+ # 8. AI tooling
+ for tool in tooling.get("ai", []):
+ ai_counts[tool] += 1
+
+ def make_items(counter: dict[str, int]) -> list[dict[str, Any]]:
+ items = []
+ for name, count in counter.items():
+ pct = round((count / total * 100), 1) if total > 0 else 0.0
+ items.append({"name": name, "count": count, "percentage": pct})
+ return sorted(items, key=lambda x: x["count"], reverse=True)
+
+ kits_items = make_items({
+ "Stellar Wallets Kit": wallets_count,
+ "Freighter API": freighter_count,
+ "OpenZeppelin Contracts": oz_count,
+ "Passkey Kit": passkey_count,
+ })
+
+ return {
+ "total_projects": total,
+ "categories": [
+ {
+ "category": "Ecosystem Kits & Wallets",
+ "description": "Adoption of official and partner Stellar libraries and kits",
+ "items": kits_items,
+ },
+ {
+ "category": "Soroban SDK Major Versions",
+ "description": "Declared soroban-sdk version in Cargo.toml contracts",
+ "items": make_items(dict(soroban_versions)),
+ },
+ {
+ "category": "Stellar SDK (JS) Versions",
+ "description": "Declared @stellar/stellar-sdk version in web applications",
+ "items": make_items(dict(stellar_versions)),
+ },
+ {
+ "category": "Frontend Frameworks",
+ "description": "Application architecture: Next.js vs Vite vs other",
+ "items": make_items(dict(framework_counts)),
+ },
+ {
+ "category": "AI Tooling & Rules",
+ "description": "Agent instructions, prompts and rules present in repository trees",
+ "items": make_items(dict(ai_counts)),
+ },
+ ],
+ }
diff --git a/apps/api/src/hackproof/routes.py b/apps/api/src/hackproof/routes.py
index 1c17422..ff06d93 100644
--- a/apps/api/src/hackproof/routes.py
+++ b/apps/api/src/hackproof/routes.py
@@ -15,6 +15,7 @@
Stats,
Tier,
TimelinePoint,
+ ToolingStats,
WasmUpload,
)
@@ -47,6 +48,12 @@ async def countries() -> list[CountryCount]:
return [CountryCount(**row) for row in await queries.fetch_countries()]
+
+
+@router.get("/tooling")
+async def tooling() -> ToolingStats:
+ return ToolingStats(**await queries.fetch_tooling())
+
@router.get("/integrations")
async def integrations() -> list[Integration]:
return [Integration(**row) for row in await queries.fetch_integrations()]
diff --git a/apps/api/src/hackproof/schemas.py b/apps/api/src/hackproof/schemas.py
index f9918d4..aff6bb7 100644
--- a/apps/api/src/hackproof/schemas.py
+++ b/apps/api/src/hackproof/schemas.py
@@ -149,3 +149,20 @@ class CountryCount(BaseModel):
class Page[T](BaseModel):
items: list[T]
total: int
+
+
+class ToolingItem(BaseModel):
+ name: str
+ count: int
+ percentage: float
+
+
+class ToolingGroup(BaseModel):
+ category: str
+ description: str
+ items: list[ToolingItem]
+
+
+class ToolingStats(BaseModel):
+ total_projects: int
+ categories: list[ToolingGroup]
diff --git a/apps/api/tests/test_dependencies.py b/apps/api/tests/test_dependencies.py
index 96e12ec..cdcee35 100644
--- a/apps/api/tests/test_dependencies.py
+++ b/apps/api/tests/test_dependencies.py
@@ -1,4 +1,13 @@
-from hackproof.github.dependencies import fingerprint
+from hackproof.github.dependencies import (
+ FREIGHTER_PACKAGES,
+ PASSKEY_PACKAGES,
+ WALLETS_KIT_PACKAGES,
+ detect_ai_tooling,
+ detect_framework,
+ detect_openzeppelin,
+ fingerprint,
+ parse_major_version,
+)
WANTED = {
"rust": ["soroban-sdk"],
@@ -54,3 +63,103 @@ def test_repos_without_stellar_dependencies_have_no_fingerprint():
assert (
fingerprint({"package.json": '{"dependencies": {"react": "19"}}'}, WANTED).ecosystems == []
)
+
+
+def test_detects_openzeppelin_in_cargo_and_package_json():
+ # 1. Cargo.toml table fixture
+ cargo_fixture = """
+ [dependencies]
+ soroban-sdk = "22.0.0"
+ openzeppelin-stellar-contracts = { version = "0.2.0" }
+ """
+ assert detect_openzeppelin({"Cargo.toml": cargo_fixture}) is True
+
+ # 2. Cargo.toml inline string fixture
+ cargo_str = """
+ [dependencies]
+ stellar-contracts = "0.1.0"
+ """
+ assert detect_openzeppelin({"Cargo.toml": cargo_str}) is True
+
+ # 3. package.json fixture
+ pkg_fixture = """
+ {
+ "dependencies": {
+ "@openzeppelin/contracts": "^5.0.0"
+ }
+ }
+ """
+ assert detect_openzeppelin({"package.json": pkg_fixture}) is True
+
+ # 4. Manifest without OpenZeppelin
+ normal_cargo = '[dependencies]\nsoroban-sdk = "22.0.0"'
+ assert detect_openzeppelin({"Cargo.toml": normal_cargo}) is False
+
+
+def test_detects_wallets_kit_and_freighter_and_passkey():
+ pkg_fixture = {
+ "package.json": """
+ {
+ "dependencies": {
+ "@creit.tech/stellar-wallets-kit": "^1.1.0",
+ "@stellar/freighter-api": "^3.0.0",
+ "passkey-kit": "^0.4.0"
+ }
+ }
+ """
+ }
+ result = fingerprint(
+ pkg_fixture,
+ {
+ "js": [
+ *WALLETS_KIT_PACKAGES,
+ *FREIGHTER_PACKAGES,
+ *PASSKEY_PACKAGES,
+ ]
+ },
+ )
+ assert "@creit.tech/stellar-wallets-kit" in result.packages
+ assert "@stellar/freighter-api" in result.packages
+ assert "passkey-kit" in result.packages
+
+
+def test_detects_next_and_vite_frameworks():
+ next_fixture = '{"dependencies": {"next": "15.0.0", "react": "19"}}'
+ assert detect_framework({"package.json": next_fixture}) == "Next.js"
+
+ vite_fixture = '{"devDependencies": {"vite": "^6.0.0", "react": "19"}}'
+ assert detect_framework({"frontend/package.json": vite_fixture}) == "Vite"
+
+ other_fixture = '{"dependencies": {"express": "^4.18.0"}}'
+ assert detect_framework({"package.json": other_fixture}) == "Other"
+
+ no_pkg = {"Cargo.toml": '[dependencies]\nsoroban-sdk = "22"'}
+ assert detect_framework(no_pkg) == "Other"
+
+
+def test_detects_ai_tooling_in_paths():
+ paths = [
+ "src/index.ts",
+ "CLAUDE.md",
+ ".agents/AGENTS.md",
+ ".cursorrules",
+ ".github/copilot-instructions.md",
+ ]
+ detected = detect_ai_tooling(paths)
+ assert "CLAUDE.md" in detected
+ assert "AGENTS.md" in detected
+ assert "Cursor rules" in detected
+ assert "Copilot instructions" in detected
+
+ empty_paths = ["src/main.rs", "Cargo.toml", "README.md"]
+ assert detect_ai_tooling(empty_paths) == []
+
+
+def test_parse_major_version():
+ assert parse_major_version("^22.0.0") == "v22"
+ assert parse_major_version("~21.3.1") == "v21"
+ assert parse_major_version("20.1") == "v20"
+ assert parse_major_version("v14.0.0") == "v14"
+ assert parse_major_version(">=13.0.0") == "v13"
+ assert parse_major_version(None) == "Unknown"
+ assert parse_major_version("git-ref") == "Unknown"
diff --git a/apps/api/tests/test_tooling_api.py b/apps/api/tests/test_tooling_api.py
new file mode 100644
index 0000000..fea3f41
--- /dev/null
+++ b/apps/api/tests/test_tooling_api.py
@@ -0,0 +1,85 @@
+from unittest.mock import AsyncMock
+
+import pytest
+from fastapi.testclient import TestClient
+
+from hackproof.main import app
+
+
+@pytest.fixture(autouse=True)
+def mock_pool_lifespan(monkeypatch):
+ monkeypatch.setattr("hackproof.main.open_pool", AsyncMock())
+ monkeypatch.setattr("hackproof.main.close_pool", AsyncMock())
+
+
+def test_tooling_api_returns_categories(monkeypatch):
+ mock_data = {
+ "total_projects": 12,
+ "categories": [
+ {
+ "category": "Ecosystem Kits & Wallets",
+ "description": "Adoption of official and partner Stellar libraries and kits",
+ "items": [
+ {"name": "Stellar Wallets Kit", "count": 6, "percentage": 50.0},
+ {"name": "Freighter API", "count": 4, "percentage": 33.3},
+ {"name": "OpenZeppelin Contracts", "count": 3, "percentage": 25.0},
+ {"name": "Passkey Kit", "count": 2, "percentage": 16.7},
+ ],
+ },
+ {
+ "category": "Soroban SDK Major Versions",
+ "description": "Declared soroban-sdk version in Cargo.toml contracts",
+ "items": [
+ {"name": "v22", "count": 8, "percentage": 66.7},
+ {"name": "v21", "count": 3, "percentage": 25.0},
+ ],
+ },
+ {
+ "category": "Stellar SDK (JS) Versions",
+ "description": "Declared @stellar/stellar-sdk version in web applications",
+ "items": [
+ {"name": "v14", "count": 7, "percentage": 58.3},
+ {"name": "v13", "count": 2, "percentage": 16.7},
+ ],
+ },
+ {
+ "category": "Frontend Frameworks",
+ "description": "Application architecture: Next.js vs Vite vs other",
+ "items": [
+ {"name": "Next.js", "count": 6, "percentage": 50.0},
+ {"name": "Vite", "count": 4, "percentage": 33.3},
+ {"name": "Other", "count": 2, "percentage": 16.7},
+ ],
+ },
+ {
+ "category": "AI Tooling & Rules",
+ "description": "Agent instructions, prompts and rules present in repository trees",
+ "items": [
+ {"name": "CLAUDE.md", "count": 5, "percentage": 41.7},
+ {"name": "Cursor rules", "count": 4, "percentage": 33.3},
+ {"name": "AGENTS.md", "count": 3, "percentage": 25.0},
+ {"name": "Copilot instructions", "count": 1, "percentage": 8.3},
+ ],
+ },
+ ],
+ }
+
+ monkeypatch.setattr("hackproof.queries.fetch_tooling", AsyncMock(return_value=mock_data))
+
+ with TestClient(app) as client:
+ resp = client.get("/api/tooling")
+ assert resp.status_code == 200
+ body = resp.json()
+ assert body["total_projects"] == 12
+ assert len(body["categories"]) == 5
+
+ categories = {cat["category"]: cat for cat in body["categories"]}
+ assert "Ecosystem Kits & Wallets" in categories
+ assert "Soroban SDK Major Versions" in categories
+ assert "Stellar SDK (JS) Versions" in categories
+ assert "Frontend Frameworks" in categories
+ assert "AI Tooling & Rules" in categories
+
+ kits = categories["Ecosystem Kits & Wallets"]["items"]
+ assert any(item["name"] == "OpenZeppelin Contracts" for item in kits)
+ assert any(item["name"] == "Stellar Wallets Kit" for item in kits)
diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx
index 1ab34f3..56d8465 100644
--- a/apps/web/src/app.tsx
+++ b/apps/web/src/app.tsx
@@ -10,6 +10,7 @@ import { SiteHeader } from '@/components/layout/site-header'
import { BuilderPage } from '@/pages/builder'
import { HomePage } from '@/pages/home'
import { ProjectPage } from '@/pages/project'
+import { ToolingPage } from '@/pages/tooling'
function Shell() {
const { view } = useChatPanel()
@@ -26,6 +27,7 @@ function Shell() {
+ {error instanceof Error ? error.message : 'Please check your connection or try again.'} +
++ What does the winning stack look like? Adoption rates across{' '} + {data.total_projects} verified + and likely hackathon projects. +
++ No packages detected yet in this category. +
+ )} +