diff --git a/apps/api/src/hackproof/routes.py b/apps/api/src/hackproof/routes.py
index 1c17422..def3931 100644
--- a/apps/api/src/hackproof/routes.py
+++ b/apps/api/src/hackproof/routes.py
@@ -1,129 +1,33 @@
-from typing import Annotated, Any, Literal
-
-from fastapi import APIRouter, HTTPException, Query
-
-from hackproof import queries
-from hackproof.schemas import (
- Builder,
- Contract,
- CountryCount,
- FailedTx,
- Integration,
- Page,
- Project,
- ProjectDetail,
- Stats,
- Tier,
- TimelinePoint,
- WasmUpload,
-)
+# Add to existing imports
+from fastapi import APIRouter
+from hackproof.db import pool
router = APIRouter()
-Search = Annotated[str | None, Query(max_length=120)]
-Tiers = Annotated[list[Tier] | None, Query()]
-Limit = Annotated[int, Query(ge=1, le=200)]
-Offset = Annotated[int, Query(ge=0)]
-EVERYTHING = 10_000
-
-
-@router.get("/stats")
-async def stats() -> Stats:
- return Stats(**await queries.fetch_stats())
-
-
-async def _page(resource: queries.Resource, q: str | None, limit: int, offset: int) -> Page[Any]:
- items, total = await queries.list_page(resource, q=q, limit=limit, offset=offset)
- return Page(items=items, total=total)
-
-
-@router.get("/timeline")
-async def timeline() -> list[TimelinePoint]:
- return [TimelinePoint(**point) for point in await queries.fetch_timeline()]
-
-
-@router.get("/countries")
-async def countries() -> list[CountryCount]:
- return [CountryCount(**row) for row in await queries.fetch_countries()]
-
-
-@router.get("/integrations")
-async def integrations() -> list[Integration]:
- return [Integration(**row) for row in await queries.fetch_integrations()]
-
-
-@router.get("/projects")
-async def projects(
- q: Search = None,
- tier: Tiers = None,
- track: Annotated[Literal["genesis", "scale"] | None, Query()] = None,
- integration: Annotated[list[str] | None, Query()] = None,
- limit: Limit = 50,
- offset: Offset = 0,
-) -> Page[Project]:
- items, total = await queries.list_page(
- "projects",
- q=q,
- tiers=list(tier) if tier else None,
- track=track,
- integrations=integration,
- limit=limit,
- offset=offset,
- )
- return Page(items=items, total=total)
-
-
-@router.get("/contracts")
-async def contracts(q: Search = None, limit: Limit = 50, offset: Offset = 0) -> Page[Contract]:
- return await _page("contracts", q, limit, offset)
-
-
-@router.get("/wasm")
-async def wasm(q: Search = None, limit: Limit = 50, offset: Offset = 0) -> Page[WasmUpload]:
- return await _page("wasm", q, limit, offset)
-
-
-@router.get("/failed")
-async def failed(q: Search = None, limit: Limit = 50, offset: Offset = 0) -> Page[FailedTx]:
- return await _page("failed", q, limit, offset)
-
-
-async def _everything(resource: queries.Resource, **filters: str) -> list[dict[str, Any]]:
- items, _ = await queries.list_page(resource, limit=EVERYTHING, **filters)
- return items
-
-
-@router.get("/projects/{project_id:path}")
-async def project(project_id: str) -> ProjectDetail:
- found = await _everything("projects", project_id=project_id)
- if not found:
- raise HTTPException(status_code=404, detail="project not found")
- return ProjectDetail(
- project=found[0],
- links=await queries.fetch_links(found[0]["full_name"]) if found[0]["full_name"] else [],
- integration_uses=(
- await queries.fetch_integration_uses(found[0]["full_name"])
- if found[0]["full_name"]
- else []
- ),
- contributors=(
- await queries.fetch_contributors(found[0]["full_name"]) if found[0]["full_name"] else []
- ),
- contracts=await _everything("contracts", project_id=project_id),
- wasm_uploads=await _everything("wasm", project_id=project_id),
- failed_txs=await _everything("failed", project_id=project_id),
- )
-
-
-@router.get("/builders/{address}")
-async def builder(address: str) -> Builder:
- result = Builder(
- address=address,
- projects=await _everything("projects", account=address),
- contracts=await _everything("contracts", account=address),
- wasm_uploads=await _everything("wasm", account=address),
- failed_txs=await _everything("failed", account=address),
- )
- if not (result.projects or result.contracts or result.wasm_uploads or result.failed_txs):
- raise HTTPException(status_code=404, detail="no activity in the hack window")
- return result
+# Add new route
+@router.get("/errors")
+async def get_error_taxonomy():
+ """Returns Soroban error taxonomy with failure counts"""
+ # Load error definitions
+ with open("config/soroban-errors.yaml") as f:
+ error_config = yaml.safe_load(f)["errors"]
+
+ # Get failure counts
+ query = """
+ SELECT result_code, COUNT(*) as count
+ FROM failed_txs
+ GROUP BY result_code
+ """
+ async with pool().acquire() as conn:
+ counts = {row["result_code"]: row["count"] for row in await conn.fetch(query)}
+
+ # Merge config with counts
+ errors = []
+ for error in error_config:
+ error_count = counts.get(error["code"], 0)
+ errors.append({
+ **error,
+ "count": error_count
+ })
+
+ return {"errors": errors}
diff --git a/apps/api/src/hackproof/tests/test_errors.yaml b/apps/api/src/hackproof/tests/test_errors.yaml
new file mode 100644
index 0000000..e0331e8
--- /dev/null
+++ b/apps/api/src/hackproof/tests/test_errors.yaml
@@ -0,0 +1,21 @@
+import yaml
+from hackproof.config import ROOT
+
+def test_error_taxonomy_completeness():
+ """Ensure all possible Soroban result codes are documented"""
+ # Load known Soroban error codes (extend this list as Soroban evolves)
+ soroban_docs = "https://soroban.stellar.org/docs/reference/errors"
+ known_codes = {
+ "TRAPPED",
+ "INSUFFICIENT_REFUNDABLE_FEE",
+ "RESOURCE_LIMIT_EXCEEDED",
+ "ENTRY_ARCHIVED",
+ # Add other known Soroban error codes here
+ }
+
+ with open(ROOT / "config" / "soroban-errors.yaml") as f:
+ config = yaml.safe_load(f)["errors"]
+ config_codes = {err["code"] for err in config}
+
+ undocumented = known_codes - config_codes
+ assert not undocumented, f"Undocumented error codes: {undocumented}"
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
new file mode 100644
index 0000000..2559583
--- /dev/null
+++ b/apps/web/src/App.tsx
@@ -0,0 +1,12 @@
+// Add to existing imports
+import ErrorsPage from "./pages/errors";
+
+// Add to route configuration
+{
+ path: "/errors",
+ element: