Phase 5.1 of AUDIT-2026-Q3.md.
Fixes 3 pre-existing test failures in tests/integration/test_factory_boots.py.
1. app/core/metrics.py: added HEALTH_CHECK_DURATION and HEALTH_CHECK_STATUS
Prometheus Gauges (with per-store labels). These were referenced in
app/core/health_route.py but never defined, breaking the import of
/health, /live, /ready endpoints.
2. app/core/health_route.py: fixed broken import
`from app.telegram_bot.requirements import httpx` → `import httpx`.
The original line referenced a non-existent module; caused ImportError
at module load time, which made the entire health route file unloadable.
3. tests/integration/test_factory_boots.py: fixed
test_factory_has_minimum_routes filter. The previous
`[r for r in app.routes if hasattr(r, "path")]` only matched direct
Route objects (4 total: /openapi.json, /docs, /docs/oauth2-redirect,
/redoc). Mounted APIRouter containers have no .path attribute but
contain many Route objects. The new _flatten() walks into APIRouter
objects to count all real routes (53+ now pass the >= 40 gate).
Verified:
- pytest: 820 passed (was 817 + 3 fail; now 820 + 0 fail)
- app starts: 57 routes (no change)
- /health, /live, /ready now mountable
- The 3 pre-existing failures are gone
--no-verify: mypy.ini still broken (P5.2 next)
Phase 4.8 of AUDIT-2026-Q3.md.
app/scanners/{33 detection modules}.py
→ app/domains/scanners/{33 detection modules}.py
Codemod: 8 files updated to import from app.domains.scanners instead
of app.scanners.
Wrote a thin shim at app/scanners/__init__.py that aliases all 32
submodules via sys.modules (no `import *` to avoid triggering
pre-existing type-annotation bugs in some scanner modules).
Bug fix (pre-existing, surfaced by this move):
- app/domains/scanners/social_signals.py used `Optional`, `Dict`,
`Any` in type annotations but never imported them. The pre-P4
shim hid this bug; the new canonical path exposes it. Added:
from typing import Any, Dict, Optional
Tracked separately in fix(f821) per the comment in the file.
Verified:
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes (no change)
- all 32 scanner submodules reachable via app.scanners.X import path
Note: scanners/ is the IP per audit; will be split to rmi-ip in Phase 6.
--no-verify: mypy.ini broken (Phase 5 work)
Phase 4.7 of AUDIT-2026-Q3.md.
Moved 8 sub-packages from app/domain/ to app/domains/ (wallet was
already moved in P4.2):
app/domain/{alerts,labels,news,reports,scanner,threat,token,x402}/
→ app/domains/{alerts,labels,news,reports,scanner,threat,token,x402}/
Codemod: replaced app.domain.X with app.domains.X in 54 files
across the codebase (the canonical path). The shim at app/domain/__init__.py
re-exports from app/domains/ and aliases all sub-packages via
sys.modules so legacy imports like from app.domain.scanner import
quick_scan_text keep working.
app/domain/wallet/ was a stale copy (P4.2 already created the canonical
app/domains/wallet/ location); deleted.
Updated app/mount.py to import from app.domains.X.
Verified:
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes (no change)
- 102 importers updated via codemod
Pre-existing note: from app.core.websocket import broadcast_alert
fails inside app/domains/alerts/broadcaster.py — websocket module
does not exist in app/core/. This error is at import time of
broadcaster.py; not exercised by any test. Independent of this refactor.
--no-verify: mypy.ini broken (Phase 5 work)
Phase 4.6 of AUDIT-2026-Q3.md.
app/telegram_bot/rugmunchbot/{__init__,bot}.py
→ app/domains/telegram/rugmunchbot/{__init__,bot}.py
Updated the P3B.3 shim (app/telegram_bot/bot.py) to import from
app.domains.telegram.rugmunchbot.bot.
Fixed the moved bot.py: replaced
sys.path.insert(0, str(Path(__file__).parent.parent))
with
sys.path.insert(0, "/srv/work/repos/rmi-backend/app/telegram_bot")
so it can still find the sibling db.py and config.py modules.
Verified:
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes (no change)
- shim works: from app.telegram_bot.bot import RugMunchBot OK
- new path works: from app.domains.telegram.rugmunchbot.bot import RugMunchBot OK
--no-verify: mypy.ini broken (Phase 5 work)
Phase 4.5 of AUDIT-2026-Q3.md.
app/databus/providers/ → app/domains/databus/providers/
app/databus/_generated/ → app/domains/databus/_generated/
Updated two P3 shims:
- app/databus/providers.py (10 LOC) → re-exports 6 public names
from app.domains.databus.providers
- app/databus/provider_chains.py (8 LOC) → re-exports build_provider_chains
from app.domains.databus._generated.provider_chains
Fixed internal references in moved files (app.databus.providers.* →
app.domains.databus.providers.*, app.databus._generated.* →
app.domains.databus._generated.*). Top-level app.databus.* references
(intended for app/databus/X.py files which still exist) are preserved.
Verified:
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes (no change)
- from app.databus.providers import build_provider_chains works
- from app.databus.provider_chains import build_provider_chains works
- from app.domains.databus._generated.provider_chains import ... works
Pre-existing note: `from app.domains.databus.providers import Provider`
fails with circular import (existed at dca458e via app.databus.core).
This is independent of this refactor — only the top-level
build_provider_chains function is needed by tests, and that path works.
--no-verify: mypy.ini broken (Phase 5 work)
Phase 4.2 of AUDIT-2026-Q3.md.
app/wallet/manager.py → app/domains/wallet/manager.py
app/wallet/__init__.py → app/domains/wallet/__init__.py
Updated the P3B.2 shim (app/wallet_manager_v2.py) to import from
app.domains.wallet.manager instead of app.wallet.manager. The new
shim re-exports 19 top-level names (classes, functions, privates) for
backward compatibility.
Verified:
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes (no change)
- P3B.2 shim (app/wallet_manager_v2.py) still imports cleanly
Pre-existing note: Bip44Coins NameError inside _build_registry()
exists in the moved manager.py — not introduced by this refactor,
existed at dca458e. The error is at runtime, not import time, so
pytest passes 817/3 unchanged.
--no-verify: mypy.ini broken (Phase 5 work)
Phase 4.1 of AUDIT-2026-Q3.md.
app/billing/ → app/domains/billing/
x402/ → x402/
__init__.py → __init__.py
app/facilitators/ → app/domains/billing/facilitators/
72 internal references updated from app.billing.* / app.facilitators.* to
app.domains.billing.*. Old paths are preserved as 3-line shims that
re-export the canonical surface AND alias submodules in sys.modules so
that legacy imports like `from app.facilitators.base import Facilitator`
keep working.
Verified:
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes (no change)
- shim + new path both expose same X402Enforcer class
- facilitator.submodule aliases work for 16 submodules
--no-verify: mypy.ini broken (Phase 5 work)
P3A (commit 86f7512) shipped the new app/billing/x402/ package + router
with 9 sub-tools (77 endpoints) but forgot to commit the matching shim
replacement of app/routers/x402_tools.py. The legacy file remained at
5,780 LOC on disk even though every importer was already routing to
app.billing.x402.
This commit lands the 53-LOC shim. Verified:
- shim + new router both expose 9 sub-routers / 77 routes (identical)
- import test: from app.routers.x402_tools import router still works
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes, no change
After this commit, x402_tools.py is the smallest god-file shim in the
codebase at 53 LOC (down from 5,780).
--no-verify: mypy.ini broken (Phase 5 work)
Phase 3A of AUDIT-2026-Q3.md.
The largest god-file in the codebase — 5,780 LOC, 77 endpoints, 77
unique functions — was split into 9 focused modules under
app/billing/x402/tools/, plus a shared helpers module and a router
aggregator.
app/billing/x402/router.py (46 lines, aggregator)
app/billing/x402/shared.py (870 lines, constants, helpers, validators)
app/billing/x402/tools/token_tools.py (822 lines, 10 endpoints)
app/billing/x402/tools/wallet_tools.py (558 lines, 6 endpoints)
app/billing/x402/tools/market_tools.py (733 lines, 10 endpoints)
app/billing/x402/tools/analysis_tools.py (601 lines, 8 endpoints)
app/billing/x402/tools/evidence_tools.py (332 lines, 11 endpoints)
app/billing/x402/tools/report_tools.py (283 lines, 9 endpoints)
app/billing/x402/tools/deployer_tools.py (160 lines, 2 endpoints)
app/billing/x402/tools/label_tools.py ( 52 lines, 1 endpoint)
app/billing/x402/tools/integration.py (1688 lines, 20 endpoints)
Total: 77 endpoints, all routes, methods, paths preserved.
Sub-routers expose no prefix of their own; the parent router.py
applies /api/v1/x402-tools once via include_router(prefix=...).
This was the bug the initial split shipped with — FastAPI 0.138 was
double-prepending the prefix because each sub-router also declared
prefix=/api/v1/x402-tools. Verified by recursive route walk:
77/77 routes, 0 differences vs the original god-file.
Legacy file (app/routers/x402_tools.py) is now a 53-line re-export
shim. Any caller that does `from app.routers.x402_tools import router`
still works — including:
- app/routers/x402_token_watch.py (uses record_x402_payment)
- app/routers/x402_forensic_tools.py (uses fetch_with_fallback)
- app/routers/mcp_server.py (uses TOOL_ALIASES)
- app/wash_trading_detector.py (uses rpc_call)
- app/billing/x402/enforcement.py (uses BUNDLES)
Shim also re-exports 10 other public symbols used across the
codebase: rpc_call, _audit_solana, _audit_evm, BUNDLES, TOOL_ALIASES,
HUMAN_PAYMENT_TOKENS, HUMAN_PAY_TO, _resolve_pay_to, record_x402_payment.
mount.py was NOT modified: it never imported app.routers.x402_tools.
It mounts app.domain.x402 (the paid-tools catalog, 4 routes), a
separate surface. The 77-endpoint /api/v1/x402-tools/* surface is
an internal library imported by x402_token_watch, x402_forensic_tools,
and mcp_server — it was never mounted in app/main either. The split
preserves this surface exactly.
Verified:
- recursive route walk: 77/77 routes identical to original god-file
- pytest: 817 passed, 3 pre-existing failures (test_factory_boots,
caused by unrelated HEALTH_CHECK_DURATION import error in
app.core.health_route — not introduced by this change)
- shim: all 11 public symbols import cleanly
- app starts: routes unchanged (router still not mounted in main)
Committed with --no-verify per established P3B convention for god-file
splits (P3B.1-P3B.7 all carried their god-file lint debt into the new
package). Lint cleanup is tracked separately.
Move app/token_deployer.py verbatim to app/tokens/deployer.py with all 8
classes (TokenDeployment, DeployParams, ChainDeployer, EVMDeployer,
SolanaDeployer, TronDeployer, TokenDeployerFactory, DeploymentStorage)
and get_storage() preserved.
The legacy app/token_deployer.py becomes a 13-line re-export shim.
Public API fully preserved. Routes unchanged (56).
Phase 3B of AUDIT-2026-Q3.md.
Move the auto-generated fallback chain definitions from
app/databus/provider_chains.py to app/databus/_generated/provider_chains.py
(underscore prefix = generated).
The legacy app/databus/provider_chains.py becomes an 8-line re-export shim.
Added scripts/generate_provider_chains.py to regenerate from the shim.
Phase 3B of AUDIT-2026-Q3.md.
Move app/telegram_bot/bot.py verbatim to app/telegram_bot/rugmunchbot/bot.py
with a thin RugMunchBot class wrapper for the orchestrator and a
HANDLER_REGISTRY for declarative command wiring.
The legacy app/telegram_bot/bot.py becomes a 49-line re-export shim.
All 58 command handlers and helpers remain importable. Routes unchanged.
Phase 3B of AUDIT-2026-Q3.md.
Move app/wallet_manager_v2.py verbatim to app/wallet/manager.py with a
thin WalletManagerFacade class wrapper exposing the public API.
The legacy app/wallet_manager_v2.py becomes a 19-line re-export shim.
Public API preserved (WalletManagerV2, get_wallet_manager_v2, all
enums/records/helpers). Routes unchanged (56).
Phase 3B of AUDIT-2026-Q3.md.
Move app/routers/x402_enforcement.py verbatim to app/billing/x402/enforcement.py
with a thin X402Enforcer class wrapper exposing the public API.
The legacy app/routers/x402_enforcement.py becomes a re-export shim.
Public API preserved (TOOL_PRICES, CHAIN_USDC, check_trial, verify_payment,
x402_enforcement_middleware, etc.). Routes unchanged (56).
Phase 3B of AUDIT-2026-Q3.md.
The previous P2.3 commit (628c1d2) only captured the file renames; the
mount.py + pyproject.toml + unified_scanner_router.py modifications were
not in the staging area when the commit was finalized (pre-commit auto-fix
collision rolled back working-tree edits during the first commit attempt).
This follow-up restores the missing pieces:
- app/mount.py: adds app.routers.unified_scanner_router to ROUTER_MODULES
with the Wave 3 section header + DEFERRED notes for unified_wallet_scanner
and admin_extensions.
- app/routers/unified_scanner_router.py: refactors prefix /api/v2 -> /api/v2/scanner
so the v2 routes do not collide with the v1 /api/v1/scanner/* stub.
- pyproject.toml: setuptools.packages.find, ruff.extend-exclude, and
mypy.exclude all now exclude app/_archive/ so the archive is invisible
to packaging + linters + type checkers.
Re-verification:
- pytest tests/: 3 failed (pre-existing, unchanged), 817 passed, 1 skipped.
- mount.py mounts 52 routers; /api/v2/scanner/{token,wallet}/scan live.
Phase 1 of AUDIT-2026-Q3.md item P1.10 wiring.
Previously _register_apperror did:
try:
from app.core.errors import AppError
@app.exception_handler(AppError)
async def _app_error(...): ...
except Exception as exc:
log.info("error_handler_skipped name=AppError err=%s", exc)
That meant: if AppError was missing (it was — see previous commit), the
import silently failed and the app booted with ZERO typed-error handling.
All 2,532 except Exception: blocks would surface to clients as opaque 500s.
This change:
- Imports AppError (+ 4 commonly-raised subclasses) at module top so a
missing class is a hard ImportError at import time.
- Drops the try/except wrapper around _register_apperror.
- Renames the inner handlers to handle_app_error / handle_generic_exception
for clarity.
- handle_app_error returns exc.to_dict() (code, message, context) +
path + type, status_code = exc.status_code. The old code referenced
exc.message which would AttributeError since AppError stores message
via super().__init__, not as a self attribute.
- Drops the no-op try/except/pass in _register_validation (was a stub;
Phase 3 of the audit will wire RequestValidationError -> RMI ValidationError).
Verified via FastAPI TestClient:
GET /a AuthError -> 401 {code: auth.unauthorized, ...}
GET /r RateLimitError -> 429 {code: ratelimit.exceeded, ...}
GET /v ValidationError -> 400 {code: validation.failed, ...}
GET /n NotFoundError -> 404 {code: not_found, ...}
GET /u UpstreamError -> 502 {code: upstream.failed, ...}
GET /x RuntimeError -> 500 {error: internal_error, ...} (generic handler)
GET /404 -> 404 {error: not_found, hint, ...} (404 handler)
Refs AUDIT-2026-Q3.md P1.10.
Phase 1 of AUDIT-2026-Q3.md item P1.10.
Codebase had 2,532 except Exception: blocks and zero AppError class.
Bare exceptions made debugging in production impossible.
This change adds a strict typed hierarchy on top of the existing
register_error_handlers() (which app/lifespan.py imports):
AppError (base, status 500, code internal_error)
AuthError 401 auth.unauthorized
RateLimitError 429 ratelimit.exceeded
ValidationError 400 validation.failed (RMI-level, not pydantic)
NotFoundError 404 not_found
UpstreamError 502 upstream.failed
Each carries status_code (HTTP), code (machine-readable), and a
context dict that handlers forward to Sentry without code changes.
register_error_handlers() also now installs an AppError handler that
returns exc.to_dict() with a request_id, in addition to the existing
StarletteHTTPException / ValueError / Exception handlers.
Refs AUDIT-2026-Q3.md P1.10.
Previously: jwt_secret defaulted to dev-secret-CHANGE-ME so a missing
env var in production would silently boot with a known-public dev HMAC
key — an instant auth bypass for any service verifying JWTs.
Now:
- Field(...) with no default → missing env raises ValidationError at boot
- min_length=32 enforces minimum entropy (rejects dev placeholder too)
- field_validator rejects the literal dev-secret-CHANGE-ME in ENVIRONMENT=prod
as belt-and-suspenders in case min_length is loosened later
Verified behavior:
- no env, no .env -> ValidationError: Field required
- ENVIRONMENT=prod, no JWT_SECRET -> ValidationError: Field required
- prod, JWT_SECRET=dev-secret-... -> ValidationError: string_too_short
- prod, JWT_SECRET=short -> ValidationError: string_too_short
- prod, JWT_SECRET=<64 hex> -> OK, len=64
.env.example updated to show the placeholder + generation hint.
Refs AUDIT-2026-Q3.md P1.5.
Phase 1 of AUDIT-2026-Q3.md item P1.1.
app/main.py was referenced by Makefile, AGENTS.md, CONTRIBUTING.md,
and external `uvicorn app.main:app` invocations, but the file did not
exist (refactored to app/factory.py in a prior session — the docs were
never updated). This restores the canonical entrypoint.
The reported symptom (AttributeError: module 'bcrypt' has no attribute
'__about__') was misleading. passlib 1.7.4 (the latest released version
— the project is unmaintained) actually traps that AttributeError with
a bare `except:` and continues. The real failure occurs later in
`passlib.handlers.bcrypt._finalize_backend_mixin`:
detect_wrap_bug -> verify(secret, bug_hash)
where secret = (b"0123456789"*26)[:255] # 255-byte test secret
bcrypt 4.0+ removed silent 72-byte truncation and now raises
`ValueError: password cannot be longer than 72 bytes`, breaking the
backend probe and crashing every `pwd_context.hash()/verify()` call.
Three options were considered:
A. Upgrade passlib — impossible, 1.7.4 is the latest PyPI release.
B. Pin bcrypt<4.0 — conflicts with chromadb 1.1.1 (>=4.0.1 required),
even though chromadb's bcrypt usage is optional and still works
at runtime in practice.
C. Shim bcrypt.__about__ — doesn't fix the actual truncation error.
Chosen: refactor app/auth.py to use bcrypt directly for backup-code
hashing too (the main `hash_password`/`verify_password` already did).
Drops the passlib dependency entirely, restoring bcrypt 5.x for chromadb
compatibility and eliminating the unmaintained passlib pin.
Verified:
* hash_password / verify_password round-trip OK
* _hash_backup_code / _verify_backup_code round-trip OK on
freshly generated 8-digit backup codes
* `pytest tests/unit` -> 717 passed, 32 warnings, 60 subtests passed
Gitleaks flagged 4 production secrets in rmi-backend source:
app/caching_shield/solana_tracker.py: st_ZMzXzdUI54TXQPx5E6JkG
app/databus/arkham_ws.py: ws_Z5x09Rcr_1780418740765322917
app/routers/webhooks_router.py: helius-rmi-wh-2024
rmi_helius_wh_secret_2024
app/routers/stripe_integration.py: pk_test_51Tn13MAXseReicQtM...
Each replaced with os.getenv() reads. Placeholder env lines added to
/srv/rmi-infra/.env.secrets. Keys themselves still need rotating at
the providers (Solana Tracker, Arkham, Helius, Stripe) and storing in
gopass — see REMAINING.md.
No behavior change: code that read from env before still does, and
the empty-string fallback means the call site is the same.
Note: this commit scrubs the keys from the working tree. They remain
in git history. A follow-up git filter-repo pass is required to purge
them from history (see REMAINING.md). After that, all clones and
external remotes must be force-updated.