Skip to content

[MISSION #80-82] Elite Search & Infrastructure Upgrade (T1 Full Suite)#797

Open
denaev-dev wants to merge 61 commits intoSolFoundry:mainfrom
denaev-dev:bounty-health-check-v2
Open

[MISSION #80-82] Elite Search & Infrastructure Upgrade (T1 Full Suite)#797
denaev-dev wants to merge 61 commits intoSolFoundry:mainfrom
denaev-dev:bounty-health-check-v2

Conversation

@denaev-dev
Copy link

@denaev-dev denaev-dev commented Mar 23, 2026

Mission #80-82: SolFoundry Elite Infrastructure Upgrade (T1)

This PR completes three Tier 1 bounties in a single comprehensive sweep, upgrading the platform's health monitoring, visual labeling system, and search capabilities to Elite standards.

🚀 Overview of Changes

🟢 Mission #80: Health Check Upgrade

  • Implemented deep system monitoring for: PostgreSQL, Redis, Solana RPC, and GitHub API Rate Limits.
  • Unified status vocabulary: healthy, degraded, unavailable.
  • Strict 200ms timeout for all I/O checks.

🟢 Mission #81: Frontend Labels Upgrade

  • Replaced basic skill tags with a premium BountyTag component.
  • Visuals: Glassmorphism styling, dynamic color palettes, and micro-animations.
  • Full layout compatibility across SkillTags and BountyCard.

🔍 Mission #82: Elite Search Engine

  • Schema: Added project_name to DB and Models, syncing with frontend expectations.
  • Hot-Relevance: New sorting engine combining text rank (ts_rank_cd) with popularity and reward logging.
  • Advanced Filtering: Added skills_logic (AND/OR) support via PostgreSQL ?& and ?|.
  • Bug Fix: Restored deadline_before filtering in the API router.
  • Aliasing: Seamless camelCase conversion for frontend consumers.

🛠 Verification

  • Verified all endpoints and UI components locally.
  • SQL Migration V2 included for schema expansion.

Wallet: BeQcMrhy5ujhakN96FDbjHnV5f844yZHq1s5AyapQSek

@coderabbitai
Copy link

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds mission/spec markdowns and multiple backend and frontend changes: implements a comprehensive async GET /api/health endpoint that concurrently checks PostgreSQL, Redis, Solana RPC, and GitHub with per-check timeouts and psutil system telemetry; introduces a HealthMonitor service and shared Redis client handling; adds a buybacks API and persistence; upgrades bounty search with project_name, skills_logic, ranking changes, and a migration to rebuild the search vector; refactors payout persistence toward PostgreSQL-first logic and expands filters; introduces Redis-backed rate-limit middleware and updated security middleware/CSP defaults; adds request instrumentation middleware and related tests; adds utility scripts and frontend tag components plus a TypeScript config tweak.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

approved, paid

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly references three specific missions (#80-82) and describes the main changes: health monitoring upgrade, frontend visual improvements, and search engine optimization to 'Elite' standards.
Description check ✅ Passed The PR description provides a comprehensive overview aligned with the changeset, detailing Mission #80 (health monitoring), #81 (frontend tags), and #82 (search engine enhancements) with specific implementation details and verification notes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 40

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
backend/app/api/bounties.py (2)

144-180: 🧹 Nitpick | 🔵 Trivial

Input validation on sort parameter.

The sort parameter (line 155) accepts any string and passes it to the search service. While bounty_search_service.py falls back to "newest" for unknown values (line 97), consider adding explicit validation with allowed values for clearer API documentation and earlier error feedback.

Suggested improvement
-    sort: str = Query("newest"),
+    sort: str = Query("newest", pattern=r"^(newest|reward_high|reward_low|deadline|submissions|best_match)$"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/bounties.py` around lines 144 - 180, The search_bounties
endpoint currently accepts any string for the sort parameter and forwards it to
BountySearchService; add explicit validation for sort by restricting it to the
allowed values used by the service (e.g. the same set handled in
BountySearchService.search/fallback logic) — update the search_bounties
signature to use a constrained type (Literal[...] or an Enum) or validate the
string at the start of the function, return a 400 on invalid values, and then
pass the validated sort value into BountySearchParams so API docs and early
errors reflect the service's accepted sort options.

154-155: ⚠️ Potential issue | 🟠 Major

Add explicit datetime parsing or validation for deadline_before parameter.

deadline_before is declared as Optional[str] in the API endpoint (line 154) but passed directly to BountySearchParams (line 175), where it's typed as Optional[datetime] (bounty.py line 417). Pydantic v2 automatically coerces valid ISO datetime strings to datetime objects, and the SQL binding receives the correct type. However, this implicit type conversion at the API boundary should be explicit—either by adding a @field_validator on deadline_before in BountySearchParams similar to the validate_sort validator, or by parsing the string in the endpoint before constructing the params object. This improves API contract clarity and makes validation behavior explicit rather than relying on Pydantic's automatic coercion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/bounties.py` around lines 154 - 155, The deadline_before
query is currently declared as Optional[str] in the API endpoint and passed into
BountySearchParams which expects Optional[datetime]; make the
conversion/validation explicit by either (A) adding a `@field_validator` on
BountySearchParams.deadline_before (mirroring validate_sort) to parse ISO
strings into datetime and raise a clear error on invalid input, or (B) parse the
string to a datetime in the endpoint before constructing BountySearchParams (in
the function that declares deadline_before and calls BountySearchParams),
ensuring you use a strict ISO parser and surface validation errors as HTTP 400.
backend/tests/test_health.py (1)

76-80: ⚠️ Potential issue | 🟠 Major

Update status code expectations for degraded health states to 503.

The implementation correctly returns 503 for degraded states (when either database or redis is disconnected), as specified in the health.py contract (line 301: http_status = 200 if core_healthy else 503). However, three tests incorrectly expect status_code == 200 when reporting degraded state:

  • test_health_check_db_down (line 76): Database disconnected, Redis up
  • test_health_check_redis_down (line 99): Database up, Redis disconnected
  • test_health_check_both_down (line 128): Both services disconnected

Since core_healthy is defined as (db_result["status"] == "healthy" and redis_result["status"] == "healthy"), any degraded scenario sets core_healthy = False, which triggers the 503 response. Update all three assertions to expect status_code == 503 instead of 200.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/test_health.py` around lines 76 - 80, The tests in
backend/tests/test_health.py are asserting 200 for degraded health responses but
the health handler sets http_status = 200 if core_healthy else 503; update the
three failing tests—test_health_check_db_down, test_health_check_redis_down, and
test_health_check_both_down—to assert response.status_code == 503 (leave the
rest of their assertions about data["status"] and services unchanged) so they
expect the correct status code when database or redis (or both) are
disconnected.
backend/app/api/payouts.py (2)

304-327: ⚠️ Potential issue | 🔴 Critical

Missing authentication on payout execution endpoint.

The execute_payout endpoint triggers on-chain SPL token transfers without authentication. Any caller who knows a payout_id can execute the transfer. This should be protected by admin authentication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/payouts.py` around lines 304 - 327, The execute_payout
endpoint lacks authentication — update the router function to require admin
authentication by adding a dependency (e.g., Depends(get_current_admin_user) or
your existing admin check) to the execute_payout signature or decorator so only
authorized admin users can call it; ensure the dependency raises appropriate
HTTP 401/403 on failure and leave the existing exception handling
(PayoutNotFoundError, InvalidPayoutTransitionError), process_payout call,
invalidate_cache(), and PayoutResponse behavior unchanged.

279-297: ⚠️ Potential issue | 🔴 Critical

Missing authentication/authorization AND critical import failures on admin approval endpoint.

The admin_approve_payout endpoint has two critical issues:

  1. Missing authentication/authorization: The endpoint accepts admin_id directly from the request body without verifying the caller's identity or authorization. Any client can submit an arbitrary admin_id. This should require authenticated admin credentials via Depends(get_current_user_id) (already defined in app.api.auth), similar to how other sensitive endpoints like those in bounties.py are protected.

  2. Broken imports — code cannot run: The endpoint attempts to import approve_payout and reject_payout from app.services.payout_service, but these functions do not exist in that file. Only 7 of 13 expected functions are defined (create_buyback, create_payout, list_buybacks, list_payouts, and helper functions exist; missing: approve_payout, reject_payout, get_payout_by_id, get_payout_by_tx_hash, get_total_paid_out, process_payout). This causes an ImportError at module initialization, making the entire payouts API router non-functional.

The service layer implementation for payout approval/rejection logic is incomplete and must be implemented before the endpoint can be used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/payouts.py` around lines 279 - 297, The admin_approve_payout
endpoint is insecure and currently breaks at import time; fix it by (1)
replacing the untrusted admin_id from AdminApprovalRequest with a dependency on
the authenticated user (use Depends(get_current_user_id) from app.api.auth) and
enforce admin authorization in admin_approve_payout (accept current_user_id via
Depends, remove or ignore body.admin_id, and pass current_user_id to
approve_payout/reject_payout), and (2) resolve the ImportError by adding the
missing service functions (approve_payout and reject_payout) to
app.services.payout_service with the expected signatures used by
admin_approve_payout (or adjust the imports to the correct function names if
they exist elsewhere); ensure the endpoint imports the implemented functions so
the module can initialize.
backend/app/services/pg_store.py (1)

37-57: ⚠️ Potential issue | 🟠 Major

The PostgreSQL mapping drops fields that PayoutResponse now exposes.

Lines 42-56 and 99-110 only persist and hydrate the legacy payout columns plus created_at. PayoutResponse now includes retry_count, failure_reason, and updated_at, but backend/app/models/tables.py has no backing columns for them and load_payouts never populates them, so loaded payouts return synthetic defaults instead of stored state.

As per coding guidelines, backend/**: "Analyze thoroughly: API contract consistency with spec".

Also applies to: 92-112

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 37 - 57, The Payout mapping is
missing new fields (retry_count, failure_reason, updated_at): update
backend/app/models/tables.py to add corresponding columns on PayoutTable, then
extend persist_payout to pass record.retry_count, record.failure_reason, and
record.updated_at into the _upsert call (use _to_uuid for bounty_id as done) and
ensure session.commit remains; finally update load_payouts to hydrate
PayoutResponse with retry_count, failure_reason, and updated_at from the DB rows
so loaded payouts reflect stored state.
backend/app/models/bounty.py (1)

402-420: ⚠️ Potential issue | 🟠 Major

skills_logic is documented as a two-value switch, but the model accepts any string.

Line 409 says this field is any or all, yet there is no enum/pattern validation. Invalid values can now flow into the search-selection path, which at best silently changes semantics and at worst becomes a query-construction hazard in the search service.

As per coding guidelines, backend/**: "Analyze thoroughly: Input validation and SQL injection vectors".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/models/bounty.py` around lines 402 - 420, The skills_logic field
on BountySearchParams currently allows any string; tighten validation by
replacing the loose Field("any", ...) with a constrained type (e.g., a
Literal["any", "all"] or a small Enum) and adjust the annotation to that type so
Pydantic enforces only "any" or "all"; update any consumers that assume str and
add a small unit test to assert invalid values raise validation errors;
reference BountySearchParams.skills_logic to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/bounties.py`:
- Line 169: The /search endpoint handler references skills_logic but that
variable is not declared, causing a NameError; add a new query parameter named
skills_logic to the function signature of the handler (the function that defines
the /search endpoint) matching the style of the other query params (same type
and default/annotation pattern used in lines 144-158) so the variable is
available where used (skills_logic=skills_logic in the return/constructor call);
ensure the parameter name exactly matches skills_logic so downstream usage stays
correct.

In `@backend/app/api/buybacks.py`:
- Around line 3-4: Remove the unused imports in backend/app/api/buybacks.py:
delete Optional and Depends from the import line so only APIRouter and
HTTPException are imported; ensure no other code in the module relies on
Optional or Depends (e.g., in any route functions or dependency declarations)
before removing them.
- Around line 15-18: The list_buybacks endpoint (function list_buybacks)
currently accepts unbounded skip and limit params; add FastAPI Query validators
to constrain them (import Query) and prevent abuse — e.g., change parameters to
skip: int = Query(0, ge=0) and limit: int = Query(100, ge=1, le=1000) (adjust
max as policy requires), then pass them through to
payout_service.list_buybacks(skip=skip, limit=limit); this ensures bounds
validation is enforced at the HTTP layer.
- Around line 10-13: The record_buyback endpoint currently allows
unauthenticated access and lacks a return type annotation; update the router
decorator and function signature to require an admin auth dependency (e.g., add
a Depends(get_current_admin) or whatever admin auth dependency your project
uses) so only authorized admins can call record_buyback, and add an explicit
return type annotation (-> BuybackResponse) to the async def
record_buyback(buyback_in: BuybackCreate) signature; ensure you pass the
authenticated admin principal into the handler if needed and continue to call
payout_service.create_buyback(buyback_in) inside the function.

In `@backend/app/api/health.py`:
- Around line 253-302: The code currently sets http_status based only on
core_healthy, which can leave a non-healthy overall_status in the response body
while returning 200; change the logic that computes http_status (variable
http_status used in the final JSONResponse) to reflect overall_status as well
(e.g., return 503 whenever overall_status is "degraded" or "unavailable",
otherwise 200) while preserving any existing special-casing for core_healthy if
needed; update the assignment of http_status near the return (the variables
core_healthy, overall_status, http_status and the final JSONResponse) so the
HTTP status code is consistent with the body.
- Around line 80-106: The health endpoints are returning raw exception strings
(e.g., str(exc)) to unauthenticated callers; update all handlers (notably the
SQLAlchemyError and generic Exception branches in the database check and the
Redis handlers in _check_redis) to return a generic, non-sensitive error value
(e.g., {"status":"unavailable","error":"infrastructure_error"} or
{"error":"unavailable"}), and ensure the full exception details are kept only in
server logs by calling logger.exception or logger.error with the exc info; do
this for the SQLAlchemyError branch, the generic Exception branches shown around
the database check and the Redis _check_redis function, and any other similar
handlers mentioned in the comment.
- Around line 121-150: The Solana/GitHub health checks use
httpx.AsyncClient(timeout=CHECK_TIMEOUT) but currently catch
asyncio.TimeoutError and return raw str(exc), which leaks internals; change the
timeout handling to use httpx.Timeout(total=CHECK_TIMEOUT) (or
httpx.Timeout(CHECK_TIMEOUT) depending on your httpx version) when constructing
httpx.AsyncClient and explicitly catch httpx.TimeoutException (and its
subclasses) instead of asyncio.TimeoutError in the exception blocks, and
sanitize responses by returning a generic error string like "timeout" or
"unavailable" (avoid returning str(exc)); update the exception handlers
surrounding httpx.AsyncClient usage (references: httpx.AsyncClient,
CHECK_TIMEOUT, httpx.TimeoutException, httpx.Timeout) to implement these
changes.

In `@backend/app/api/payouts.py`:
- Line 58: The import get_total_paid_out is unused in this module; remove
get_total_paid_out from the import list in backend/app/api/payouts.py (where the
current from ... import ... list includes get_total_paid_out) to eliminate the
unused import, or if you intended to use it, call get_total_paid_out in the
appropriate handler (e.g., within the payout endpoints) and ensure tests/linter
pass; after the change, run the formatter and linter to confirm no remaining
unused imports.

In `@backend/app/main.py`:
- Around line 36-42: The CORS middleware call using app.add_middleware with
CORSMiddleware sets allow_credentials=True while using wildcards
(allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) which is
invalid; fix by either (A) providing explicit origin(s) in allow_origins and
replacing wildcard allow_methods/allow_headers with explicit lists (e.g.,
["GET","POST",...]) while keeping allow_credentials=True, or (B) if you must
allow any origin, set allow_credentials=False; update the call site in main.py
(the app.add_middleware(CORSMiddleware, ... ) invocation) accordingly so the
configuration matches FastAPI/Starlette rules.

In `@backend/app/middleware/rate_limit.py`:
- Around line 59-64: The _get_client_id function currently trusts the first
X-Forwarded-For value which allows clients to spoof rate-limit buckets; change
it to only trust XFF after validating trusted proxy hops: accept a configured
TRUSTED_PROXIES set (or CIDRs) and compute the client IP by splitting
X-Forwarded-For into a list, appending request.client.host as the last hop, then
iterating from the rightmost hop removing entries that match TRUSTED_PROXIES
until a non-trusted hop remains — use that hop as the client id; also validate
entries are well-formed IPs and fall back to request.client.host or "unknown"
when validation fails (update _get_client_id to implement this logic and
reference TRUSTED_PROXIES where appropriate).

In `@backend/app/middleware/security.py`:
- Around line 35-50: The dispatch method trusts the first X-Forwarded-For hop
(client_ip) allowing spoofing; change client IP resolution to only accept/parse
X-Forwarded-For when the immediate peer (request.client.host) is a trusted
proxy. Add a trusted_proxies configuration (e.g., set or list) and in dispatch:
if request.client.host is in trusted_proxies, parse X-Forwarded-For safely
(choose appropriate hop—usually the left-most original client), otherwise use
request.client.host (or "unknown"); then call redis.sismember("ip_blocklist",
client_ip) as before. Ensure references: dispatch, client_ip,
request.headers["X-Forwarded-For"], request.client.host, trusted_proxies, and
redis.sismember("ip_blocklist") are updated accordingly.
- Around line 20-27: The Content-Security-Policy set in
SecurityMiddleware.dispatch is too restrictive and breaks FastAPI's /docs
Swagger UI; update the CSP header logic in dispatch to either skip/relax the
policy for FastAPI docs paths or add explicit script-src and style-src
directives that allow the Swagger UI resources (e.g., include
https://cdn.jsdelivr.net and the necessary inline script allowance via a safer
mechanism such as a nonce/hash or, if not feasible, 'unsafe-inline' as a
temporary measure) and permit fonts/images as needed (e.g., font-src and
img-src); locate the dispatch method in SecurityMiddleware and modify the
response.headers["Content-Security-Policy"] assignment to implement the
relaxed/conditional policy (or conditional bypass) so /docs loads correctly
while keeping strict defaults for other routes.

In `@backend/app/models/bounty.py`:
- Around line 246-251: The camel-case aliasing is only applied in BountyBase so
models that inherit BaseModel (BountyUpdate, BountyListItem, BountySearchResult,
BountySearchParams) remain snake_case in the API contract; to fix, either have
these models inherit from BountyBase or add the same model_config
("populate_by_name": True and the alias_generator lambda) to each of those
classes (BountyUpdate, BountyListItem, BountySearchResult, BountySearchParams)
so incoming/outgoing fields use the expected camelCase aliases consistently with
the frontend contract.
- Around line 210-214: The project_name Field in the Pydantic model accepts
unbounded strings but the DB column is String(100), so update the project_name
Field in the Bounty model (symbol: project_name in the Bounty model in
backend/app/models/bounty.py) to enforce a 100-character limit (e.g., add
max_length=100 or use a constrained string type) and apply the same change to
the other occurrence referenced (the second project_name definition around line
288); keep the description/examples the same and ensure validation prevents
>100-char values before persistence to match
backend/app/models/bounty_table.py's String(100).

In `@backend/app/services/health.py`:
- Around line 34-44: Move the `_running` check inside the `track_request` lock
to avoid a TOCTOU race: acquire `self._lock` first, then check `self._running`
before updating metrics in `track_request`; reference `_running`, `_lock`, and
`track_request` to locate the change. Also prevent unbounded growth of
`self._path_counts` by normalizing or bounding keys: either canonicalize dynamic
segments (e.g., strip/replace IDs from `path` before using it) or enforce a
fixed-size container (LRU/eviction) for `_path_counts` so new unique/random
paths evict old entries; reference `_path_counts` when applying the
normalization or replacement eviction strategy. Ensure error counting and status
code updates remain inside the same locked region.

In `@backend/app/services/payout_service.py`:
- Around line 110-126: The current duplicate-check in create_buyback (using
pg_store.load_buybacks limited to 1000) can miss older or concurrent duplicates
and lets the DB unique constraint on BuybackTable.tx_hash surface as a 500;
change create_buyback to treat the pre-scan as best-effort but wrap the persist
call (pg_store.persist_buyback) in an exception handler that catches the DB
unique-violation/IntegrityError thrown for BuybackTable.tx_hash and raises an
HTTPException(status_code=400, detail="Buyback with this tx_hash already
exists.") instead; keep or expand the pre-scan if desired but ensure the
DB-level uniqueness error is translated to a 400 to handle races and older
duplicates.
- Around line 47-72: The duplicate-check is racy and limited by
load_payouts(limit=1000); update create_payout to stop scanning and instead
query for an existing payout by bounty_id (e.g., add/use a
pg_store.get_payout_by_bounty(bounty_id) or equivalent) and rely on a
database-level uniqueness constraint on bounty_id in
backend/app/models/tables.py (add a UNIQUE index/constraint for the bounty_id
column). Make the persistence robust by adding the UNIQUE constraint and keeping
the insert via pg_store.persist_payout(PayoutRecord...) but wrap the persist
call to catch the DB integrity error (e.g., IntegrityError) and convert it to
HTTPException(400, ...) for duplicate bounty; this ensures concurrent requests
cannot create duplicate payouts and removes the fragile load_payouts scanning
logic in create_payout.
- Around line 75-108: The code currently truncates reads by calling
pg_store.load_payouts(limit=10000) in list_payouts and get_payout which hides
older payouts; update these functions to use proper store-level operations: in
list_payouts call a pg_store method that implements server-side filtering and
pagination (e.g., pg_store.load_payouts with skip and limit parameters or a new
pg_store.query_payouts(status, recipient, skip, limit)) instead of forcing
limit=10000, and in get_payout call a direct lookup on the store (e.g.,
pg_store.get_payout_by_id(payout_id)) rather than loading all payouts; modify
list_payouts to forward skip/limit and filters to the store and modify
get_payout to fetch the single payout by id and raise 404 if not found.

In `@backend/app/services/pg_store.py`:
- Around line 180-200: persist_bounty is not persisting the bounty.category
field so category data accepted by the API is lost; update the call to _upsert
inside the persist_bounty function to include category=bounty.category (or
bounty.category.value if it's an enum) alongside the other kwargs so
BountyTable's category column is written; reference the persist_bounty function
and BountyTable/BountyBase types to locate where to add the category parameter
and ensure any enum/value handling matches how status is handled.

In `@backend/migrations/V2__add_project_name_and_rebuild_search_vector.sql`:
- Around line 43-44: The migration's full-table UPDATE ("UPDATE bounties SET
updated_at = now()") will lock the entire bounties table; change it to perform
the update in small batches (e.g., loop over primary key ranges or use
LIMIT/ctid batches) or run it in a maintenance window so each batch updates a
subset and commits between batches, ensuring the search_vector rebuild is
triggered without a long-running transaction; update the migration SQL in
V2__add_project_name_and_rebuild_search_vector.sql to implement the batched
UPDATE approach targeting the bounties table.
- Around line 17-26: Remove the dead/overwritten first definition of the trigger
function bounties_search_trigger (the CREATE OR REPLACE FUNCTION block that sets
search_vector using json_array_elements_text) so only the intended/final
definition remains; also eliminate the invalid use of the set-returning function
json_array_elements_text in a scalar assignment (ensure the retained function
builds search_vector from project_name/title/description/skills using a safe
scalar approach instead of directly calling json_array_elements_text).
- Around line 9-13: The second CREATE OR REPLACE FUNCTION that builds the
search_vector is missing aggregation of the skills JSON array and is overwriting
the earlier version; update the active function (the CREATE OR REPLACE FUNCTION
that defines the trigger for jobs/job postings) to aggregate skills via
something like array_to_string(ARRAY(SELECT
jsonb_array_elements_text(NEW.skills)), ' ') and include it as a weight D
component: add setweight(to_tsvector('simple', coalesce(skills_text, '')), 'D')
to the concatenation used for NEW.search_vector (or merge into the existing
tsvector combination), and remove or replace the duplicate earlier function so
only the corrected function is defined and executed.

In `@backend/requirements.txt`:
- Around line 20-22: The three new requirements lack upper bounds and the
suggested constraints were incorrect; update the dependency specs for anthropic,
openai, and psutil to include conservative upper bounds: change
"anthropic>=0.40.0" to "anthropic>=0.40.0,<1.0.0", change "openai>=1.50.0" to
"openai>=1.50.0,<3.0.0", and change "psutil>=5.9.0" to "psutil>=5.9.0,<8.0.0" so
they remain consistent with the project's pinned-style requirements while
allowing the current stable releases.

In `@backend/scripts/setup.sh`:
- Around line 79-81: The syntax check command currently targets "../main.py"
(the line with python3 -m py_compile ../main.py); update that argument to point
to the FastAPI app file at "../app/main.py" so the py_compile step verifies
backend/app/main.py instead of backend/main.py, keeping the surrounding echo and
color variables (GREEN/NC) intact.
- Around line 58-69: The virtual environment is being created using the VENV_DIR
value (referenced as "$VENV_DIR") in the scripts block but the requirements are
installed from "../requirements.txt", which indicates the venv is being created
in the wrong directory; update the script so VENV_DIR points to the backend root
(e.g., set VENV_DIR to "../.venv" or create the venv at the parent dir) and then
source "$VENV_DIR/bin/activate" and run pip install -r
"$VENV_DIR/../requirements.txt" (or simply pip install -r ../requirements.txt
after creating the venv in the parent) to ensure the virtualenv location and the
requirements file path are consistent (modify the VENV_DIR initialization and
the venv creation/activation lines that reference "$VENV_DIR").
- Around line 44-53: Make the path handling in setup.sh robust by defining
SCRIPT_DIR (e.g., resolve the script's directory via BASH_SOURCE) and use it to
build absolute paths for ENV_EXAMPLE and ENV_FILE instead of literal
"../../$ENV_EXAMPLE" and "../$ENV_FILE"; update the existence checks and the cp
invocation that reference ENV_EXAMPLE and ENV_FILE to use the
SCRIPT_DIR-prefixed absolute paths so the script works regardless of the current
working directory, leaving the conditional logic and messages (the if checks and
echo lines) intact.

In `@backend/submit_bounty.py`:
- Around line 1-7: Remove the unused import of the os module at the top of the
file; in the import block that currently includes "import os", keep only the
actually used imports ("import subprocess" and "import logging") so there is no
dead code left, and verify imports around logging.basicConfig and logger =
logging.getLogger("stark_submit") remain unchanged.
- Around line 25-36: The current automated git steps (logger.info and the
subprocess.run calls that perform "git add .", "git commit ...", and "git push
... --force") are dangerous: replace the indiscriminate git add with explicit
staged paths or validate and filter changed files (don't use "git add ."),
remove automatic "--force" pushes and require an explicit flag/confirmation to
allow force, and avoid hardcoding the branch by deriving the target branch from
an argument or environment variable and aborting if it doesn't match the
expected PR source (add a dry-run mode and interactive/CLI confirmation in the
script before running subprocess.run for commit/push). Ensure these safeguards
are enforced around the subprocess.run calls and log via
logger.info/logger.error when aborting.
- Around line 1-39: The file backend/submit_bounty.py should be removed from
this PR because it is unrelated and risky; delete backend/submit_bounty.py from
the branch (or if this automation is intentionally needed, move it to a
dedicated scripts/ or tools/ directory), remove unused imports/variables
(pr_body, os) and replace unsafe git commands in run() (subprocess.run(["git",
"add", "."]), subprocess.run(["git", "push", "origin",
"bounty-169-final-certified", "--force"])) with explicit staged file paths,
interactive/confirmed push (no --force), and parameterized inputs (no hardcoded
wallet or bounty references), keeping logger and run() only if the script is
moved and sanitized.

In `@backend/tests/test_security_mission.py`:
- Around line 10-24: The test_ip_blocklist is a no-op because it never asserts
the response and patches redis after the app/middleware were instantiated;
remove the trailing pass, ensure the Redis mock is applied to the actual
middleware instance before making the request (either patch the module used by
IPBlocklistMiddleware earlier or set IPBlocklistMiddleware.redis = mock_redis
before creating AsyncClient), then perform the request and assert the middleware
behavior (e.g., response.status_code == 403 and the JSON payload contains the
IP_BLOCKED detail). Target symbols: test_ip_blocklist, IPBlocklistMiddleware,
app, AsyncClient, and the patched redis.from_url/mock_redis.

In `@backend/verify_middleware.py`:
- Around line 23-29: The test in the Payload Size Limit block posts to
"/api/sync" which may not exist and will produce 404 instead of 413; update the
check in the test that builds large_body and calls ac.post("/api/sync", ...) to
first assert/examine resp_size.status_code for 404 and handle that case (e.g.,
skip the 413 assertion or log that the endpoint is missing), or switch the
request to an endpoint known to exist in the suite; specifically modify the code
around the ac.post("/api/sync", content=large_body) call and the
resp_size.status_code handling so the test treats 404 as “endpoint missing” and
only treats 413 as the payload-size enforcement success.
- Around line 31-34: The rate-limit audit is currently reading headers from resp
(the /health GET response) instead of the most recent /api/sync POST response;
change the header reads in the Rate Limit Header Audit block to use
resp_size.headers (e.g., replace resp.headers.get(...) with
resp_size.headers.get(...)) so X-RateLimit-Limit and X-RateLimit-Remaining
reflect the POST response that followed the payload test; keep the same header
keys and fallback values.
- Line 4: Remove the unused import by deleting the line "import json" from
backend/verify_middleware.py (or, if JSON functionality is intended, replace it
with actual usage in the relevant function such as any verify middleware
functions), ensuring no other code references the json symbol; this removes the
unused import warning and keeps the module clean.

In `@frontend/src/components/bounties/SkillTags.tsx`:
- Around line 10-13: The mapped BountyTag instances in SkillTags.tsx use
key={skill}, which can collide when skills repeat; update the map call to
include the index (e.g., visible.map((skill, i) => ...)) and set a unique key
like key={`${skill}-${i}`} or, if each skill has an identifier, use that id
(e.g., key={skill.id}) so keys are stable and unique for BountyTag components.
- Around line 5-7: Normalize the maxVisible prop before using it: convert it to
a number, floor it to an integer, clamp it to a minimum of 0 (and optionally to
skills.length), then use that normalized value everywhere (replace uses of
maxVisible in the computations for visible and overflow and the other occurrence
around lines 17-20). For example, compute a single const like normalizedMax =
Math.max(0, Math.floor(Number(maxVisible))) (or clamp to Math.min(skills.length,
... ) if desired) and use normalizedMax for skills.slice(...) and for
calculating overflow so negative or fractional inputs can't produce incorrect UI
badges; update all references (e.g., visible, overflow, and any rendering logic)
to use normalizedMax.

In `@frontend/src/components/common/BountyTag.tsx`:
- Around line 39-46: The BountyTag component currently attaches onClick to a
<span> making it inaccessible to keyboard users; update the render so when
onClick is provided the element uses proper interactive semantics (e.g., render
a <button> or add role="button" + tabIndex={0} and keyDown handler) to support
Enter/Space activation and focus styles; ensure to keep existing
className/styleClasses and preserve cursor/hover/active behavior and update any
handlers in BountyTag to call onClick(label) for both mouse and keyboard
activation.
- Around line 23-25: The current substring checks in BountyTag (variable
"lower") use includes('api') and includes('ui') which cause false positives;
change the logic to match whole tokens instead (e.g., split the tag string into
tokens on non-word characters or whitespace and check token equality, or use
word-boundary regex like /\bapi\b/ and /\bui\b/), update the conditional checks
that reference "lower" accordingly so only exact tag tokens (api, ui, frontend,
backend, security, audit) trigger their respective style returns, and keep the
existing return strings unchanged.
- Around line 33-34: The fallback currently returns invalid Tailwind tokens like
`bg-hsl(${hue}, ...)` (the function that builds/returns these style class
strings and the runtime check `styleClasses.includes('hsl')` must be updated);
replace the malformed classes with Tailwind arbitrary-value syntax (e.g.
`bg-[hsl(${hue}_70%_50%_/_0.1)] text-[hsl(${hue}_70%_70%)]
border-[hsl(${hue}_70%_50%_/_0.3)]`) OR remove the dynamic class fallback
entirely and apply the computed HSL/HSLA values via the component’s inline
`style` prop (compute hsla strings from `hue` and set backgroundColor, color,
borderColor), and remove the fragile regex/string-detection logic that checks
`styleClasses.includes('hsl')`.

---

Outside diff comments:
In `@backend/app/api/bounties.py`:
- Around line 144-180: The search_bounties endpoint currently accepts any string
for the sort parameter and forwards it to BountySearchService; add explicit
validation for sort by restricting it to the allowed values used by the service
(e.g. the same set handled in BountySearchService.search/fallback logic) —
update the search_bounties signature to use a constrained type (Literal[...] or
an Enum) or validate the string at the start of the function, return a 400 on
invalid values, and then pass the validated sort value into BountySearchParams
so API docs and early errors reflect the service's accepted sort options.
- Around line 154-155: The deadline_before query is currently declared as
Optional[str] in the API endpoint and passed into BountySearchParams which
expects Optional[datetime]; make the conversion/validation explicit by either
(A) adding a `@field_validator` on BountySearchParams.deadline_before (mirroring
validate_sort) to parse ISO strings into datetime and raise a clear error on
invalid input, or (B) parse the string to a datetime in the endpoint before
constructing BountySearchParams (in the function that declares deadline_before
and calls BountySearchParams), ensuring you use a strict ISO parser and surface
validation errors as HTTP 400.

In `@backend/app/api/payouts.py`:
- Around line 304-327: The execute_payout endpoint lacks authentication — update
the router function to require admin authentication by adding a dependency
(e.g., Depends(get_current_admin_user) or your existing admin check) to the
execute_payout signature or decorator so only authorized admin users can call
it; ensure the dependency raises appropriate HTTP 401/403 on failure and leave
the existing exception handling (PayoutNotFoundError,
InvalidPayoutTransitionError), process_payout call, invalidate_cache(), and
PayoutResponse behavior unchanged.
- Around line 279-297: The admin_approve_payout endpoint is insecure and
currently breaks at import time; fix it by (1) replacing the untrusted admin_id
from AdminApprovalRequest with a dependency on the authenticated user (use
Depends(get_current_user_id) from app.api.auth) and enforce admin authorization
in admin_approve_payout (accept current_user_id via Depends, remove or ignore
body.admin_id, and pass current_user_id to approve_payout/reject_payout), and
(2) resolve the ImportError by adding the missing service functions
(approve_payout and reject_payout) to app.services.payout_service with the
expected signatures used by admin_approve_payout (or adjust the imports to the
correct function names if they exist elsewhere); ensure the endpoint imports the
implemented functions so the module can initialize.

In `@backend/app/models/bounty.py`:
- Around line 402-420: The skills_logic field on BountySearchParams currently
allows any string; tighten validation by replacing the loose Field("any", ...)
with a constrained type (e.g., a Literal["any", "all"] or a small Enum) and
adjust the annotation to that type so Pydantic enforces only "any" or "all";
update any consumers that assume str and add a small unit test to assert invalid
values raise validation errors; reference BountySearchParams.skills_logic to
locate the change.

In `@backend/app/services/pg_store.py`:
- Around line 37-57: The Payout mapping is missing new fields (retry_count,
failure_reason, updated_at): update backend/app/models/tables.py to add
corresponding columns on PayoutTable, then extend persist_payout to pass
record.retry_count, record.failure_reason, and record.updated_at into the
_upsert call (use _to_uuid for bounty_id as done) and ensure session.commit
remains; finally update load_payouts to hydrate PayoutResponse with retry_count,
failure_reason, and updated_at from the DB rows so loaded payouts reflect stored
state.

In `@backend/tests/test_health.py`:
- Around line 76-80: The tests in backend/tests/test_health.py are asserting 200
for degraded health responses but the health handler sets http_status = 200 if
core_healthy else 503; update the three failing tests—test_health_check_db_down,
test_health_check_redis_down, and test_health_check_both_down—to assert
response.status_code == 503 (leave the rest of their assertions about
data["status"] and services unchanged) so they expect the correct status code
when database or redis (or both) are disconnected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8f498ba0-fc48-4c9f-a91b-23b81d671cb9

📥 Commits

Reviewing files that changed from the base of the PR and between 72d63b0 and 3e496e5.

📒 Files selected for processing (28)
  • Mission_80_Prompt.md
  • Mission_80_Specs.md
  • Mission_82_Prompt.md
  • Mission_82_Specs.md
  • backend/app/api/bounties.py
  • backend/app/api/buybacks.py
  • backend/app/api/health.py
  • backend/app/api/payouts.py
  • backend/app/constants.py
  • backend/app/main.py
  • backend/app/middleware/rate_limit.py
  • backend/app/middleware/security.py
  • backend/app/models/bounty.py
  • backend/app/models/bounty_table.py
  • backend/app/services/bounty_search_service.py
  • backend/app/services/health.py
  • backend/app/services/payout_service.py
  • backend/app/services/pg_store.py
  • backend/migrations/V2__add_project_name_and_rebuild_search_vector.sql
  • backend/requirements.txt
  • backend/scripts/setup.sh
  • backend/submit_bounty.py
  • backend/tests/test_health.py
  • backend/tests/test_security_mission.py
  • backend/verify_middleware.py
  • frontend/src/components/bounties/SkillTags.tsx
  • frontend/src/components/common/BountyTag.tsx
  • frontend/tsconfig.json

Comment on lines +3 to +4
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unused imports: Optional and Depends are imported but never used.

Proposed fix
-from typing import Optional
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, HTTPException
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/buybacks.py` around lines 3 - 4, Remove the unused imports in
backend/app/api/buybacks.py: delete Optional and Depends from the import line so
only APIRouter and HTTPException are imported; ensure no other code in the
module relies on Optional or Depends (e.g., in any route functions or dependency
declarations) before removing them.

Comment on lines +10 to +13
@router.post("/", response_model=BuybackResponse, summary="Record a buyback")
async def record_buyback(buyback_in: BuybackCreate):
"""Record a manual SOL -> FNDRY buyback event and update treasury stats."""
return await payout_service.create_buyback(buyback_in)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing authentication on buyback recording endpoint.

The record_buyback endpoint allows anyone to record buyback events without authentication. This is a treasury-impacting operation that should require admin authorization.

Additionally, the function lacks a return type annotation for consistency with FastAPI conventions.

Proposed fix for type annotation
 `@router.post`("/", response_model=BuybackResponse, summary="Record a buyback")
-async def record_buyback(buyback_in: BuybackCreate):
+async def record_buyback(buyback_in: BuybackCreate) -> BuybackResponse:
     """Record a manual SOL -> FNDRY buyback event and update treasury stats."""
     return await payout_service.create_buyback(buyback_in)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/buybacks.py` around lines 10 - 13, The record_buyback
endpoint currently allows unauthenticated access and lacks a return type
annotation; update the router decorator and function signature to require an
admin auth dependency (e.g., add a Depends(get_current_admin) or whatever admin
auth dependency your project uses) so only authorized admins can call
record_buyback, and add an explicit return type annotation (-> BuybackResponse)
to the async def record_buyback(buyback_in: BuybackCreate) signature; ensure you
pass the authenticated admin principal into the handler if needed and continue
to call payout_service.create_buyback(buyback_in) inside the function.

Comment on lines +15 to +18
@router.get("/", response_model=BuybackListResponse, summary="List buybacks")
async def list_buybacks(skip: int = 0, limit: int = 100):
"""Retrieve a paginated list of all recorded buyback events."""
return await payout_service.list_buybacks(skip=skip, limit=limit)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing input validation bounds on pagination parameters.

The skip and limit parameters lack validation constraints. Without bounds, a malicious client could request limit=999999999 causing performance issues or memory exhaustion.

Proposed fix with validation
 `@router.get`("/", response_model=BuybackListResponse, summary="List buybacks")
-async def list_buybacks(skip: int = 0, limit: int = 100):
+async def list_buybacks(
+    skip: int = Query(0, ge=0, description="Number of records to skip"),
+    limit: int = Query(100, ge=1, le=1000, description="Maximum records per page"),
+) -> BuybackListResponse:
     """Retrieve a paginated list of all recorded buyback events."""
     return await payout_service.list_buybacks(skip=skip, limit=limit)

Note: You'll need to import Query from fastapi.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/buybacks.py` around lines 15 - 18, The list_buybacks endpoint
(function list_buybacks) currently accepts unbounded skip and limit params; add
FastAPI Query validators to constrain them (import Query) and prevent abuse —
e.g., change parameters to skip: int = Query(0, ge=0) and limit: int =
Query(100, ge=1, le=1000) (adjust max as policy requires), then pass them
through to payout_service.list_buybacks(skip=skip, limit=limit); this ensures
bounds validation is enforced at the HTTP layer.

Comment on lines +5 to +7
const visible = skills.slice(0, maxVisible);
const overflow = skills.length - maxVisible;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

maxVisible should be normalized before slicing/counting overflow.

Lines 5-7 compute visibility/overflow directly from maxVisible (typed as number). Negative or fractional values can produce invalid UI output (e.g., incorrect +overflow badge values).

As per coding guidelines, frontend/**: “React/TypeScript frontend. Check: Component structure and state management”.

Also applies to: 17-20

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/bounties/SkillTags.tsx` around lines 5 - 7, Normalize
the maxVisible prop before using it: convert it to a number, floor it to an
integer, clamp it to a minimum of 0 (and optionally to skills.length), then use
that normalized value everywhere (replace uses of maxVisible in the computations
for visible and overflow and the other occurrence around lines 17-20). For
example, compute a single const like normalizedMax = Math.max(0,
Math.floor(Number(maxVisible))) (or clamp to Math.min(skills.length, ... ) if
desired) and use normalizedMax for skills.slice(...) and for calculating
overflow so negative or fractional inputs can't produce incorrect UI badges;
update all references (e.g., visible, overflow, and any rendering logic) to use
normalizedMax.

Comment on lines +10 to +13
{visible.map(skill => (
<BountyTag
key={skill}
label={skill}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate key risk in mapped tags.

Line 12 uses key={skill}, which is not unique when skills repeat. This can cause incorrect tag reuse/reordering during re-renders.

As per coding guidelines, frontend/**: “React/TypeScript frontend. Check: Component structure and state management”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/bounties/SkillTags.tsx` around lines 10 - 13, The
mapped BountyTag instances in SkillTags.tsx use key={skill}, which can collide
when skills repeat; update the map call to include the index (e.g.,
visible.map((skill, i) => ...)) and set a unique key like key={`${skill}-${i}`}
or, if each skill has an identifier, use that id (e.g., key={skill.id}) so keys
are stable and unique for BountyTag components.

Comment on lines +23 to +25
if (lower.includes('backend') || lower.includes('api')) return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
if (lower.includes('frontend') || lower.includes('ui')) return 'bg-pink-500/20 text-pink-400 border-pink-500/30';
if (lower.includes('security') || lower.includes('audit')) return 'bg-red-500/20 text-red-400 border-red-500/30';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Over-broad substring matching causes false style categorization.

Lines 23-25 use includes('api') and especially includes('ui'), which can match unrelated words and assign incorrect color categories.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/BountyTag.tsx` around lines 23 - 25, The
current substring checks in BountyTag (variable "lower") use includes('api') and
includes('ui') which cause false positives; change the logic to match whole
tokens instead (e.g., split the tag string into tokens on non-word characters or
whitespace and check token equality, or use word-boundary regex like /\bapi\b/
and /\bui\b/), update the conditional checks that reference "lower" accordingly
so only exact tag tokens (api, ui, frontend, backend, security, audit) trigger
their respective style returns, and keep the existing return strings unchanged.

Comment on lines +33 to +34
return `bg-hsl(${hue}, 70%, 50%, 0.1) text-hsl(${hue}, 70%, 70%) border-hsl(${hue}, 70%, 50%, 0.3)`;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Tailwind CSS v4, are class names like bg-hsl(210, 70%, 50%, 0.1)valid utilities, or must HSL colors be expressed via arbitrary value syntax (e.g.,bg-[hsl(...)]) or inline styles?

💡 Result:

bg-hsl(210, 70%, 50%, 0.1) is not a valid Tailwind utility in v4.

Tailwind v4 only made some utilities accept “dynamic” unbracketed values (like grid-cols-15), but color utilities still expect a named color token (like bg-sky-500) unless you use arbitrary values. (tailwindcss.com)

Use one of these instead:

  • Arbitrary value (recommended for one-offs): bg-[hsl(210_70%_50%_/_0.1)] (underscores stand in for spaces in class names)
  • Or define a theme variable (then you can use bg-yourcolor) (tailwindcss.com)
  • Or inline styles (especially if the value is truly dynamic at runtime) (tailwindcss.com)

Citations:


🏁 Script executed:

cat -n frontend/src/components/common/BountyTag.tsx | head -60

Repository: SolFoundry/solfoundry

Length of output: 2895


Fallback color styling uses invalid Tailwind tokens with runtime string-parsing fallback.

Lines 33 generate invalid Tailwind class tokens (bg-hsl(${hue}, 70%, 50%, 0.1) etc.) that do not match Tailwind v4 syntax. While lines 48–52 work around this with a runtime string detection (styleClasses.includes('hsl')) and extract the hue value via regex to apply inline hsla styles, this pattern is fragile and relies on string parsing rather than proper styling declarations.

Instead, use one of these approaches:

  1. Arbitrary values syntax: Return bg-[hsl(${hue}_70%_50%_/_0.1)] text-[hsl(${hue}_70%_70%)] border-[hsl(${hue}_70%_50%_/_0.3)] (underscores replace spaces in class names)
  2. Pure inline styles: Remove the className fallback entirely and apply all dynamic colors via the style prop

The current workaround avoids build-time errors but creates runtime fragility and string-manipulation overhead that reduces maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/BountyTag.tsx` around lines 33 - 34, The
fallback currently returns invalid Tailwind tokens like `bg-hsl(${hue}, ...)`
(the function that builds/returns these style class strings and the runtime
check `styleClasses.includes('hsl')` must be updated); replace the malformed
classes with Tailwind arbitrary-value syntax (e.g.
`bg-[hsl(${hue}_70%_50%_/_0.1)] text-[hsl(${hue}_70%_70%)]
border-[hsl(${hue}_70%_50%_/_0.3)]`) OR remove the dynamic class fallback
entirely and apply the computed HSL/HSLA values via the component’s inline
`style` prop (compute hsla strings from `hue` and set backgroundColor, color,
borderColor), and remove the fragile regex/string-detection logic that checks
`styleClasses.includes('hsl')`.

Comment on lines +39 to +46
<span
onClick={() => onClick?.(label)}
className={`
inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-medium
border backdrop-blur-sm transition-all duration-200
${styleClasses}
${onClick ? 'cursor-pointer hover:scale-105 active:scale-95' : ''}
${className}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Interactive tag is not keyboard-accessible.

Lines 39-46 attach click behavior to a <span> without button semantics/focus handling. When onClick is provided, this blocks keyboard-only interaction for tag actions.

As per coding guidelines, frontend/**: “React/TypeScript frontend. Check: Component structure and state management”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/BountyTag.tsx` around lines 39 - 46, The
BountyTag component currently attaches onClick to a <span> making it
inaccessible to keyboard users; update the render so when onClick is provided
the element uses proper interactive semantics (e.g., render a <button> or add
role="button" + tabIndex={0} and keyDown handler) to support Enter/Space
activation and focus styles; ensure to keep existing className/styleClasses and
preserve cursor/hover/active behavior and update any handlers in BountyTag to
call onClick(label) for both mouse and keyboard activation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
backend/app/api/payouts.py (1)

107-144: ⚠️ Potential issue | 🔴 Critical

Financial mutation endpoints remain publicly callable.

record_payout, record_buyback, admin_approve_payout, and execute_payout expose treasury-changing operations without any auth or admin check. An unauthenticated caller can create records, approve/reject payouts, or trigger payout execution.

As per coding guidelines, backend/**: "Analyze thoroughly: Authentication/authorization gaps".

Also applies to: 177-212

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/payouts.py` around lines 107 - 144, The financial endpoints
(record_payout, record_buyback, admin_approve_payout, execute_payout) are
publicly callable; add an authentication/authorization dependency that enforces
admin privileges before executing mutations. For each function (record_payout,
record_buyback, admin_approve_payout, execute_payout) add a FastAPI dependency
parameter (e.g., current_user: User = Depends(get_current_user) or admin: User =
Depends(require_admin)) and either call a shared require_admin() dependency or
explicitly check current_user.is_admin and raise HTTPException(403) on failure;
keep existing error handling and invalidate_cache() calls intact so only
authenticated admins can perform these treasury-changing operations.
backend/tests/test_health.py (1)

57-69: ⚠️ Potential issue | 🟠 Major

The health contract test still encodes the pre-upgrade schema.

The added assertions only cover metadata. Lines 60-61 still pin database/redis to connected/disconnected, and there are no checks for the new Solana/GitHub probes or the Mission #80 timeout behavior. This can either fail against the upgraded contract or let an off-spec health response pass.

As per coding guidelines, backend/**: "Analyze thoroughly: API contract consistency with spec".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/test_health.py` around lines 57 - 69, The test in
backend/tests/test_health.py currently hardcodes service statuses
(data["services"]["database"] == "connected" and data["services"]["redis"] ==
"connected") and misses new probes and mission-timeout behavior; update the
assertions to validate the health contract per spec by: checking
data["services"] exists and contains keys for "database", "redis", "solana", and
"github"; assert each service value is one of the allowed status tokens (e.g.,
"connected", "disconnected", "degraded", "unknown") rather than exact strings;
keep the metadata checks (version, uptime_seconds >= 0, timestamp endswith "Z");
and add an assertion for the Mission `#80` timeout indicator (e.g., presence and
boolean status like data.get("mission_timeout") or the spec-defined key) so the
test will correctly accept upgraded contract responses while still validating
required fields.
backend/app/api/bounties.py (1)

210-214: ⚠️ Potential issue | 🟠 Major

Add input validation for deadline_before and skills_logic query parameters at the API boundary.

deadline_before is typed as Optional[str] in the handler parameter but Optional[datetime] in BountySearchParams. This means validation is deferred until BountySearchParams() is constructed inside the handler (line 221), causing malformed datetimes to raise ValidationError as 500 errors instead of 422 responses.

skills_logic lacks any validation. It defaults to "any" but has no field validator. In the search service, any value other than exactly "all" silently triggers OR semantics (?| operator, line 62 of bounty_search_service.py). Invalid values should be rejected at the boundary, not silently degraded.

Compare with sort and category, which use @field_validator to enforce allowed values. Apply the same pattern to skills_logic and change deadline_before: Optional[str] to deadline_before: Optional[datetime] in the handler signature to validate at the FastAPI boundary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/bounties.py` around lines 210 - 214, Change the handler
parameter deadline_before from Optional[str] to Optional[datetime] so FastAPI
performs ISO datetime validation at the API boundary (update the handler
signature for the endpoint in bounties.py and import datetime), and add a field
validator for skills_logic inside BountySearchParams (the same pattern used for
sort/category) that only allows "any" or "all" and raises ValueError for other
values so invalid skills_logic values are rejected at input rather than silently
treated as OR in bounty_search_service.py.
backend/app/services/pg_store.py (4)

232-246: ⚠️ Potential issue | 🟡 Minor

persist_buyback uses _upsert, risking tx_hash unique constraint violation.

Similar to payouts, BuybackTable.tx_hash has a unique constraint. If an upsert changes the tx_hash to one that already exists on another row, an IntegrityError will be raised but not handled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 232 - 246, persist_buyback
currently calls _upsert which can raise an IntegrityError if
BuybackTable.tx_hash uniqueness is violated; wrap the _upsert + session.commit()
in a try/except that catches sqlalchemy.exc.IntegrityError, perform
session.rollback() on error, and handle the conflict deterministically (e.g.,
query BuybackTable by tx_hash to skip/update the existing row or log and return)
so the IntegrityError is not unhandled; reference persist_buyback, _upsert, and
BuybackTable.tx_hash when implementing the catch and conflict-resolution logic.

189-208: ⚠️ Potential issue | 🟡 Minor

persist_payout uses _upsert but doesn't handle unique constraint on tx_hash.

PayoutTable.tx_hash has a unique=True constraint (per backend/app/models/tables.py). If _upsert updates an existing row with a tx_hash that conflicts with another row's tx_hash, the database will raise an IntegrityError. This exception is not caught and will propagate as a 500 error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 189 - 208, persist_payout
calls _upsert which can trigger a unique constraint violation on
PayoutTable.tx_hash; wrap the _upsert/commit in a try/except that catches
sqlalchemy.exc.IntegrityError, and inside the except check whether the error is
a tx_hash uniqueness conflict (use the exception message or inspect orig) and
handle it gracefully—either look up the existing PayoutTable row by tx_hash and
update that row instead of inserting (using the session and _to_uuid/record.id
logic) or raise a controlled application error; ensure you import IntegrityError
from sqlalchemy.exc and maintain session rollback/cleanup when catching the
exception so the DB session isn’t left in a bad state.

283-302: ⚠️ Potential issue | 🟡 Minor

load_reputation has no pagination, loading all history into memory.

Line 288 selects all ReputationHistoryTable rows without limit. For systems with extensive history, this could cause memory exhaustion and slow response times.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 283 - 302, The load_reputation
function loads all ReputationHistoryTable rows into memory which can OOM; change
it to iterate in paginated/batched streaming mode using the DB session instead
of loading all scalars at once. Inside load_reputation (and within the async
with get_db_session() block) replace the single await
session.execute(stmt)).scalars() call with a batched fetch/streaming loop (e.g.,
use session.stream/select with execution_options or explicit LIMIT/OFFSET or
cursor-based paging) that yields rows in chunks and processes each chunk into
ReputationHistoryEntry before appending to out; keep the same field mappings
(entry_id, contributor_id, bounty_id, bounty_title, bounty_tier, review_score,
earned_reputation, anti_farming_applied, created_at) and preserve the order_by
ReputationHistoryTable.created_at.desc() while ensuring the loop stops when no
more rows are returned.

21-30: 🧹 Nitpick | 🔵 Trivial

_upsert lacks conflict handling for concurrent updates.

The get-then-update pattern (lines 24-30) is not atomic. Two concurrent calls for the same pk_value could both see obj is None, leading to duplicate insert attempts. While PostgreSQL will reject the duplicate PK, this surfaces as an unhandled exception.

For true upsert semantics, consider using SQLAlchemy's insert().on_conflict_do_update() or PostgreSQL's INSERT ... ON CONFLICT.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 21 - 30, The _upsert function
uses a non-atomic get-then-add pattern that can race on concurrent calls;
replace it with a true DB-level upsert by using SQLAlchemy Core's
insert().on_conflict_do_update() against the model class table: convert pk_value
via _to_uuid, build an insert statement from model_cls.__table__ (include
id=pk_value and the columns), call on_conflict_do_update with the primary
key/index (e.g., index_elements=[model_cls.id] or the actual PK column) and set_
to the incoming columns, then execute the statement with await
session.execute(...) (and flush/commit as currently expected) instead of
session.get/session.add to avoid duplicate-insert errors.
♻️ Duplicate comments (8)
backend/requirements.txt (1)

27-29: ⚠️ Potential issue | 🟠 Major

Missing upper bounds on new dependencies creates upgrade instability.

Line 27, Line 28, and Line 29 add anthropic, openai, and psutil with only lower bounds, while the rest of this file consistently uses capped ranges. This can allow silent major-version jumps and break runtime/API compatibility in production.

#!/bin/bash
set -euo pipefail

echo "1) Confirm current specs in backend/requirements.txt"
rg -n '^(anthropic|openai|psutil)' backend/requirements.txt

echo
echo "2) Fetch latest published versions from PyPI (for cap selection)"
for pkg in anthropic openai psutil; do
  echo "== $pkg =="
  curl -fsSL "https://pypi.org/pypi/${pkg}/json" | jq -r '.info.version'
done

echo
echo "3) Quick policy consistency check: show bounded vs unbounded entries"
python - <<'PY'
from pathlib import Path
import re
p = Path("backend/requirements.txt")
lines = p.read_text().splitlines()
for i,l in enumerate(lines,1):
    if re.match(r'^(anthropic|openai|psutil)>=', l):
        bounded = '<' in l
        print(f"Line {i}: {l} | upper_bound={'yes' if bounded else 'no'}")
PY

As per coding guidelines, "**: Be DETAILED and SPECIFIC ... for automated LLM pipeline."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/requirements.txt` around lines 27 - 29, The three new dependency
lines for anthropic, openai, and psutil in backend/requirements.txt lack upper
bounds and must be changed to match the project's bounded-range style; update
each entry (anthropic, openai, psutil) to use a capped range like
">=current_min,<next_major" (or "<=latest_patch" if the file uses minor/patch
caps) so upgrades cannot jump major versions silently—identify the current
minimum versions you added and set the corresponding upper-bound cap consistent
with other entries in requirements.txt, then run the same PyPI/version check
used in the review to verify the chosen caps.
backend/app/models/bounty.py (2)

236-240: ⚠️ Potential issue | 🟠 Major

project_name still accepts values the table cannot store.

backend/app/models/bounty_table.py caps the column at String(100), but both BountyBase and BountyUpdate accept unbounded strings. Oversize payloads will clear model validation and fail only at persistence time.

As per coding guidelines, backend/**: "Analyze thoroughly: Input validation and SQL injection vectors".

Also applies to: 316-316

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/models/bounty.py` around lines 236 - 240, Bounty models accept
unbounded project_name while the DB column is String(100); update the Field
definition(s) named project_name in both BountyBase and BountyUpdate to enforce
max length 100 (e.g., Field(..., max_length=100) or use
pydantic.constr(max_length=100)) so validation fails early and matches
backend/app/models/bounty_table.py's String(100).

272-277: ⚠️ Potential issue | 🟠 Major

CamelCase aliasing still stops at BountyBase.

BountyUpdate, BountyListItem/BountySearchResult, and BountySearchParams still inherit directly from BaseModel, so patch/search/list flows keep exposing snake_case fields like project_name, deadline_before, and skills_logic. The frontend contract is only partially migrated.

As per coding guidelines, backend/**: "Analyze thoroughly: API contract consistency with spec".

Also applies to: 305-317, 408-425, 464-483

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/models/bounty.py` around lines 272 - 277, Several Bounty-related
models (BountyUpdate, BountyListItem, BountySearchResult, BountySearchParams)
still inherit directly from BaseModel and thus do not pick up the camelCase
aliasing defined in the shared model_config; update each of those classes to
inherit from BountyBase (the class that defines model_config with
populate_by_name and alias_generator) so they automatically use camelCase
aliases, or alternatively copy the same model_config into each class if
inheritance is not possible; update BountyUpdate, BountyListItem,
BountySearchResult, and BountySearchParams accordingly so fields like
project_name, deadline_before, and skills_logic are exposed as camelCase to the
frontend.
backend/app/main.py (1)

128-134: ⚠️ Potential issue | 🟠 Major

This CORS configuration is still invalid for credentialed requests.

Starlette/FastAPI do not support allow_credentials=True with wildcard origins, methods, and headers. Browsers will block credentialed cross-origin calls even though the server boots.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/main.py` around lines 128 - 134, The CORS setup in
app.add_middleware using CORSMiddleware is invalid because
allow_credentials=True cannot be used with wildcard origins; update the
configuration for CORSMiddleware by replacing allow_origins=["*"] with an
explicit list of allowed origins (or use allow_origin_regex with a safe regex),
or set allow_credentials=False if wildcards are required; keep allow_methods and
allow_headers as needed but ensure allow_origins is a concrete list (or regex)
whenever allow_credentials=True so credentialed requests are not blocked by
browsers.
backend/app/middleware/security.py (1)

35-43: ⚠️ Potential issue | 🟠 Major

The CSP is still incorrect for production.

script-src and style-src still omit the CDN hosts FastAPI’s default /docs UI depends on, so Swagger/ReDoc remain broken. At the same time, the global 'unsafe-inline' 'unsafe-eval' allowances weaken the policy for every other route instead of isolating that relaxation to docs/static pages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/middleware/security.py` around lines 35 - 43, Update the global
CSP constants in middleware/security.py so the default CSP_SCRIPT_SRC and
CSP_STYLE_SRC include the CDN hosts used by FastAPI docs (e.g.,
cdn.jsdelivr.net, unpkg.com, and cdnjs.cloudflare.com and keep
fonts.googleapis.com/font sources) but remove the global "'unsafe-inline'
'unsafe-eval'" allowances; then add dedicated CSP_DOCS_SCRIPT_SRC and
CSP_DOCS_STYLE_SRC (or similar) that include the CDN hosts plus the relaxed
"'unsafe-inline' 'unsafe-eval'" only for the docs/static endpoints, and update
the docs/static response middleware or route that sets the
Content-Security-Policy header to use these docs-specific variables while
leaving CSP_SCRIPT_SRC and CSP_STYLE_SRC strict for all other routes (reference
CSP_SCRIPT_SRC, CSP_STYLE_SRC, CSP_DOCS_SCRIPT_SRC/CSP_DOCS_STYLE_SRC and the
middleware/security.py header-setting logic).
backend/app/services/payout_service.py (3)

63-75: ⚠️ Potential issue | 🔴 Critical

Duplicate-check race condition and incomplete scan remain unaddressed.

The bounty-level duplicate check at lines 70-75 loads only 5000 payouts, missing older records. Combined with the non-atomic check-then-insert pattern, concurrent requests can race past each other. The PayoutTable.bounty_id column lacks a unique constraint (per backend/app/models/tables.py), so the DB won't catch duplicates either.

Additionally, the in-memory tx_hash check (lines 63-68) only scans _payout_store, which may be stale or incomplete relative to the database.

Recommended fix: Add a UNIQUE constraint on PayoutTable.bounty_id (for non-null values) and wrap pg_store.persist_payout in a try/except to catch IntegrityError, converting it to an HTTPException(400). Remove the fragile pre-scan logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 63 - 75, Remove the
fragile in-memory and limited pre-scan duplicate checks (the loop over
_payout_store under _lock and the partial pg_store.load_payouts scan) and
instead enforce uniqueness at the DB level by adding a UNIQUE constraint on
PayoutTable.bounty_id (for non-null values); then make persist calls use the DB
as source-of-truth by wrapping pg_store.persist_payout in a try/except that
catches the DB IntegrityError (or the specific SQLAlchemy/asyncpg integrity
exception used) and raises HTTPException(status_code=400, detail="Bounty already
has a payout" or "Payout with tx_hash already exists" as appropriate); keep
references to data.tx_hash and data.bounty_id but remove the pre-insert
duplicate scans so concurrent requests rely on the DB constraint and the
IntegrityError-to-HTTPException conversion.

142-145: ⚠️ Potential issue | 🟠 Major

Payout lookups silently truncate at fixed limits, causing false 404s.

get_payout_by_id (line 142) and get_payout_by_tx_hash (line 149) load only 5000/10000 records respectively. Once the table exceeds these limits, valid older payouts will not be found, returning None and causing false "not found" responses to callers.

Recommended fix: Implement direct database lookups (session.get() for ID, indexed query for tx_hash) instead of loading all records and filtering in Python.

Also applies to: 149-153

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 142 - 145,
get_payout_by_id and get_payout_by_tx_hash currently call
pg_store.load_payouts(limit=...) and filter in Python which truncates results
and yields false 404s; change get_payout_by_id to load the single payout by
primary key via the DB session (use session.get(Payout, payout_id) or the
equivalent store method) and change get_payout_by_tx_hash to run a direct
indexed query (e.g.
session.query(Payout).filter_by(tx_hash=tx_hash).one_or_none() or the equivalent
store method) instead of iterating over pg_store.load_payouts; return
_payout_to_response(...) when a row is found and preserve the None return when
not found.

243-259: ⚠️ Potential issue | 🟠 Major

Buyback duplicate check has same race condition and truncation issues.

Line 244 loads only 1000 buybacks for duplicate detection. BuybackTable.tx_hash has a UNIQUE constraint (per backend/app/models/tables.py), so duplicates outside the scan window—or concurrent inserts—will cause an unhandled database IntegrityError surfacing as a 500 error instead of the intended 400.

Recommended fix: Wrap pg_store.persist_buyback (line 256) in a try/except to catch the unique violation and convert to HTTPException(400).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 243 - 259, The
duplicate-check using load_buybacks(limit=1000) is insufficient; wrap the call
to pg_store.persist_buyback(record) in a try/except that catches the DB
unique-constraint/IntegrityError (or the specific DB driver unique-violation
exception) and re-raise HTTPException(status_code=400, detail="Buyback already
exists"); only add the record to _buyback_store under _lock after
persist_buyback succeeds (so use BuybackRecord, pg_store.persist_buyback,
BuybackTable.tx_hash, _buyback_store, and _lock to locate and update the code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/payouts.py`:
- Around line 155-163: The success branch of validate_wallet currently omits the
computed is_program flag so known program addresses are returned as ordinary
valid wallets; update the success return in validate_wallet (and ensure both
branches use the same response shape) to include is_program_address=is_program
in the WalletValidationResponse so callers can detect program accounts;
reference validate_wallet and WalletValidationResponse to locate and modify the
response construction.

In `@backend/app/main.py`:
- Around line 190-208: The contributors_router endpoints were never included in
the app registration block; add app.include_router(contributors_router,
prefix="/api", tags=["contributors"]) to the router registration sequence in
main.py (near the other app.include_router calls such as auth_router,
bounties_router, and contributor_webhooks_router) so the imported
contributors_router is actually mounted and its endpoints become available at
the /api prefix.
- Around line 138-140: The health-check endpoint moved to /api/v2/health but the
middleware exemptions in the IPBlocklistMiddleware, RateLimitMiddleware, and
RateLimiterMiddleware still only whitelist /health or /api/health, causing
probes to be blocked; update each middleware's exemption logic (in the classes
IPBlocklistMiddleware, RateLimitMiddleware, and RateLimiterMiddleware) to also
treat "/api/v2/health" as a permitted path (and preserve the existing /health
and /api/health checks), ensuring the middlewares skip blocking/rate-limiting
for that exact route so health probes are not 403/429'd.
- Around line 136-161: The add_request_id_and_timing middleware must reuse any
request ID already set by prior middleware (LoggingMiddleware) instead of
minting a new one; modify add_request_id_and_timing to first check
request.state.request_id and use it if present, otherwise fall back to
request.headers.get("X-Request-ID") or generate uuid.uuid4(), then assign that
value to request.state.request_id and set response.headers["X-Request-ID"] (but
only if not already set) so LoggingMiddleware, add_request_id_and_timing,
monitor.track_request, and any error payloads all reference the same ID.

In `@backend/app/services/bounty_search_service.py`:
- Around line 61-64: The SQL branch uses "?&" for skills_logic == "all" but the
in-memory fallback still does OR-only matching; update the in-memory filtering
that examines params.skills and params.skills_logic (the code that iterates over
bounty 'b' and checks b.skills in the fallback near the current binds/conditions
logic) to perform AND semantics when params.skills_logic == "all" (e.g., require
all requested skills are present via subset check) and OR semantics otherwise
(require any overlap), so results match the PostgreSQL path for both
skills_logic modes.

In `@backend/app/services/payout_service.py`:
- Around line 181-190: The reject_payout function updates the DB via
pg_store.persist_payout but fails to update the in-memory cache (_payout_store),
causing inconsistency; after awaiting pg_store.persist_payout(record) in
reject_payout, update the _payout_store entry for payout_id to reflect the
changed record (same approach used in approve_payout), including status,
failure_reason, and updated_at so the in-memory cache matches the persisted
payout.
- Around line 288-290: The get_total_paid_out function currently calls
pg_store.load_payouts(limit=10000) which truncates results; change it to compute
the total across all payouts instead of only the first 10k—either by (A)
invoking a DB-side aggregate (preferred) such as adding/using a method like
pg_store.sum_payouts_by_status(status=PayoutStatus.CONFIRMED) to return a single
summed value, or (B) implementing pagination in get_total_paid_out that
repeatedly calls pg_store.load_payouts(...) (removing the fixed 10000 cap) and
accumulates sum(p.amount for p in payouts.values() if p.status ==
PayoutStatus.CONFIRMED) until no more pages; update get_total_paid_out to use
the chosen approach and reference PayoutStatus.CONFIRMED and
pg_store.load_payouts or the new pg_store.sum_payouts_by_status method.
- Around line 261-270: The total returned by list_buybacks is wrong because it
derives total from the truncated items list; update list_buybacks (and/or
pg_store.load_buybacks) so you obtain the true total count rather than
len(items): either call a dedicated count method (e.g.,
pg_store.count_buybacks()) and set total to that value, or change
pg_store.load_buybacks to return a tuple/object with (items_map, total) and use
that total when constructing BuybackListResponse; ensure you still slice the
returned items for pagination and map with _buyback_to_response.
- Around line 272-282: The startup hydration in hydrate_from_database currently
uses hardcoded limits (pg_store.load_payouts(limit=5000),
load_buybacks(limit=1000)) which can miss records; change it to fetch all rows
by paginating or streaming instead of a single fixed-limit call: replace the
single calls with a loop that repeatedly calls pg_store.load_payouts(batch_size,
offset/last_id) and pg_store.load_buybacks(batch_size, offset/last_id) until no
more rows are returned, updating _payout_store and _buyback_store inside the
existing _lock on each batch, keep the final log.info to report total counts,
and preserve the try/except and log.error behavior in hydrate_from_database.
- Around line 203-211: The code currently calls
pg_store.load_payouts(limit=5000) and then checks all_p.get(payout_id) which can
return a false 404 if the payout is beyond the limit, and it persists status
change with pg_store.persist_payout(record) but delays the cache update until
later, leaving a window of stale cache; change the DB fetch to load the specific
payout by id (e.g., add or use a pg_store.load_payout(payout_id) or a filtered
query instead of load_payouts(limit=5000)), keep the existing validation of
record.status, set record.status = PayoutStatus.PROCESSING and
record.updated_at, call await pg_store.persist_payout(record), and immediately
update the cache (the same cache update logic from lines 233-234) right after
persist_payout to eliminate the stale-window before continuing the rest of
process_payout.
- Around line 110-127: The code currently fetches an unbounded set with
pg_store.load_payouts(limit=limit + skip + 1000) and then applies in-memory
filters (recipient, status, bounty_id, token, start_date, end_date), causing
incorrect total counts and memory/performance issues; update
pg_store.load_payouts to accept filter parameters (recipient, status, bounty_id,
token, start_date, end_date, skip, limit) and have it perform the WHERE clauses
and a separate COUNT(*) query so that total reflects the true DB count, then use
the returned page results to populate page and total instead of filtering items
in-memory.
- Around line 157-172: The code currently operates on a possibly stale cached
record from _payout_store (locked by _lock) and persists it without reconciling
with the DB; instead, fetch the authoritative DB record (via
pg_store.load_payouts(...) and .get(payout_id) or a dedicated loader) and use
that for the status check and mutation (PayoutStatus checks and setting
record.status/updated_at), then await pg_store.persist_payout(db_record) and
finally update the in-memory cache _payout_store inside the same _lock so the
cache reflects the persisted state; reference symbols: _lock, _payout_store,
payout_id, pg_store.load_payouts, persist_payout, PayoutStatus, updated_at.

In `@backend/app/services/pg_store.py`:
- Around line 277-281: The save_last_sync function currently always inserts a
new SyncStateTable row (session.add(SyncStateTable(last_sync=dt))) causing
unbounded growth; change it to perform an upsert/update instead: locate the
existing SyncStateTable row (by its single known key/unique constraint or
primary id) and update its last_sync value, or use your ORM's upsert support
(e.g., insert().on_conflict_do_update or session.merge) to atomically set
last_sync = dt, then commit; update the code paths inside save_last_sync and
keep using get_db_session() for the transaction.
- Around line 224-226: The code currently constructs
PayoutStatus(row.status.lower()) which will raise ValueError for unexpected
values; wrap that conversion in a try/except ValueError, log the invalid status
along with identifying fields from the row (e.g., row.tx_hash or row.id), and
assign a safe fallback (use PayoutStatus.UNKNOWN if that enum member exists,
otherwise assign None or a designated default) so the load operation doesn't
crash; also consider adding a note to validate/normalize status values when
persisting.
- Around line 16-19: The helper _to_uuid currently swallows
ValueError/AttributeError and returns the original value on failure; change it
to surface conversion failures by raising a clear exception (e.g., ValueError
with the offending value and context) instead of returning the original, or at
minimum log a warning before returning; update the except block in _to_uuid to
raise a descriptive ValueError (or call the logger) so callers know the UUID
conversion failed.

---

Outside diff comments:
In `@backend/app/api/bounties.py`:
- Around line 210-214: Change the handler parameter deadline_before from
Optional[str] to Optional[datetime] so FastAPI performs ISO datetime validation
at the API boundary (update the handler signature for the endpoint in
bounties.py and import datetime), and add a field validator for skills_logic
inside BountySearchParams (the same pattern used for sort/category) that only
allows "any" or "all" and raises ValueError for other values so invalid
skills_logic values are rejected at input rather than silently treated as OR in
bounty_search_service.py.

In `@backend/app/api/payouts.py`:
- Around line 107-144: The financial endpoints (record_payout, record_buyback,
admin_approve_payout, execute_payout) are publicly callable; add an
authentication/authorization dependency that enforces admin privileges before
executing mutations. For each function (record_payout, record_buyback,
admin_approve_payout, execute_payout) add a FastAPI dependency parameter (e.g.,
current_user: User = Depends(get_current_user) or admin: User =
Depends(require_admin)) and either call a shared require_admin() dependency or
explicitly check current_user.is_admin and raise HTTPException(403) on failure;
keep existing error handling and invalidate_cache() calls intact so only
authenticated admins can perform these treasury-changing operations.

In `@backend/app/services/pg_store.py`:
- Around line 232-246: persist_buyback currently calls _upsert which can raise
an IntegrityError if BuybackTable.tx_hash uniqueness is violated; wrap the
_upsert + session.commit() in a try/except that catches
sqlalchemy.exc.IntegrityError, perform session.rollback() on error, and handle
the conflict deterministically (e.g., query BuybackTable by tx_hash to
skip/update the existing row or log and return) so the IntegrityError is not
unhandled; reference persist_buyback, _upsert, and BuybackTable.tx_hash when
implementing the catch and conflict-resolution logic.
- Around line 189-208: persist_payout calls _upsert which can trigger a unique
constraint violation on PayoutTable.tx_hash; wrap the _upsert/commit in a
try/except that catches sqlalchemy.exc.IntegrityError, and inside the except
check whether the error is a tx_hash uniqueness conflict (use the exception
message or inspect orig) and handle it gracefully—either look up the existing
PayoutTable row by tx_hash and update that row instead of inserting (using the
session and _to_uuid/record.id logic) or raise a controlled application error;
ensure you import IntegrityError from sqlalchemy.exc and maintain session
rollback/cleanup when catching the exception so the DB session isn’t left in a
bad state.
- Around line 283-302: The load_reputation function loads all
ReputationHistoryTable rows into memory which can OOM; change it to iterate in
paginated/batched streaming mode using the DB session instead of loading all
scalars at once. Inside load_reputation (and within the async with
get_db_session() block) replace the single await
session.execute(stmt)).scalars() call with a batched fetch/streaming loop (e.g.,
use session.stream/select with execution_options or explicit LIMIT/OFFSET or
cursor-based paging) that yields rows in chunks and processes each chunk into
ReputationHistoryEntry before appending to out; keep the same field mappings
(entry_id, contributor_id, bounty_id, bounty_title, bounty_tier, review_score,
earned_reputation, anti_farming_applied, created_at) and preserve the order_by
ReputationHistoryTable.created_at.desc() while ensuring the loop stops when no
more rows are returned.
- Around line 21-30: The _upsert function uses a non-atomic get-then-add pattern
that can race on concurrent calls; replace it with a true DB-level upsert by
using SQLAlchemy Core's insert().on_conflict_do_update() against the model class
table: convert pk_value via _to_uuid, build an insert statement from
model_cls.__table__ (include id=pk_value and the columns), call
on_conflict_do_update with the primary key/index (e.g.,
index_elements=[model_cls.id] or the actual PK column) and set_ to the incoming
columns, then execute the statement with await session.execute(...) (and
flush/commit as currently expected) instead of session.get/session.add to avoid
duplicate-insert errors.

In `@backend/tests/test_health.py`:
- Around line 57-69: The test in backend/tests/test_health.py currently
hardcodes service statuses (data["services"]["database"] == "connected" and
data["services"]["redis"] == "connected") and misses new probes and
mission-timeout behavior; update the assertions to validate the health contract
per spec by: checking data["services"] exists and contains keys for "database",
"redis", "solana", and "github"; assert each service value is one of the allowed
status tokens (e.g., "connected", "disconnected", "degraded", "unknown") rather
than exact strings; keep the metadata checks (version, uptime_seconds >= 0,
timestamp endswith "Z"); and add an assertion for the Mission `#80` timeout
indicator (e.g., presence and boolean status like data.get("mission_timeout") or
the spec-defined key) so the test will correctly accept upgraded contract
responses while still validating required fields.

---

Duplicate comments:
In `@backend/app/main.py`:
- Around line 128-134: The CORS setup in app.add_middleware using CORSMiddleware
is invalid because allow_credentials=True cannot be used with wildcard origins;
update the configuration for CORSMiddleware by replacing allow_origins=["*"]
with an explicit list of allowed origins (or use allow_origin_regex with a safe
regex), or set allow_credentials=False if wildcards are required; keep
allow_methods and allow_headers as needed but ensure allow_origins is a concrete
list (or regex) whenever allow_credentials=True so credentialed requests are not
blocked by browsers.

In `@backend/app/middleware/security.py`:
- Around line 35-43: Update the global CSP constants in middleware/security.py
so the default CSP_SCRIPT_SRC and CSP_STYLE_SRC include the CDN hosts used by
FastAPI docs (e.g., cdn.jsdelivr.net, unpkg.com, and cdnjs.cloudflare.com and
keep fonts.googleapis.com/font sources) but remove the global "'unsafe-inline'
'unsafe-eval'" allowances; then add dedicated CSP_DOCS_SCRIPT_SRC and
CSP_DOCS_STYLE_SRC (or similar) that include the CDN hosts plus the relaxed
"'unsafe-inline' 'unsafe-eval'" only for the docs/static endpoints, and update
the docs/static response middleware or route that sets the
Content-Security-Policy header to use these docs-specific variables while
leaving CSP_SCRIPT_SRC and CSP_STYLE_SRC strict for all other routes (reference
CSP_SCRIPT_SRC, CSP_STYLE_SRC, CSP_DOCS_SCRIPT_SRC/CSP_DOCS_STYLE_SRC and the
middleware/security.py header-setting logic).

In `@backend/app/models/bounty.py`:
- Around line 236-240: Bounty models accept unbounded project_name while the DB
column is String(100); update the Field definition(s) named project_name in both
BountyBase and BountyUpdate to enforce max length 100 (e.g., Field(...,
max_length=100) or use pydantic.constr(max_length=100)) so validation fails
early and matches backend/app/models/bounty_table.py's String(100).
- Around line 272-277: Several Bounty-related models (BountyUpdate,
BountyListItem, BountySearchResult, BountySearchParams) still inherit directly
from BaseModel and thus do not pick up the camelCase aliasing defined in the
shared model_config; update each of those classes to inherit from BountyBase
(the class that defines model_config with populate_by_name and alias_generator)
so they automatically use camelCase aliases, or alternatively copy the same
model_config into each class if inheritance is not possible; update
BountyUpdate, BountyListItem, BountySearchResult, and BountySearchParams
accordingly so fields like project_name, deadline_before, and skills_logic are
exposed as camelCase to the frontend.

In `@backend/app/services/payout_service.py`:
- Around line 63-75: Remove the fragile in-memory and limited pre-scan duplicate
checks (the loop over _payout_store under _lock and the partial
pg_store.load_payouts scan) and instead enforce uniqueness at the DB level by
adding a UNIQUE constraint on PayoutTable.bounty_id (for non-null values); then
make persist calls use the DB as source-of-truth by wrapping
pg_store.persist_payout in a try/except that catches the DB IntegrityError (or
the specific SQLAlchemy/asyncpg integrity exception used) and raises
HTTPException(status_code=400, detail="Bounty already has a payout" or "Payout
with tx_hash already exists" as appropriate); keep references to data.tx_hash
and data.bounty_id but remove the pre-insert duplicate scans so concurrent
requests rely on the DB constraint and the IntegrityError-to-HTTPException
conversion.
- Around line 142-145: get_payout_by_id and get_payout_by_tx_hash currently call
pg_store.load_payouts(limit=...) and filter in Python which truncates results
and yields false 404s; change get_payout_by_id to load the single payout by
primary key via the DB session (use session.get(Payout, payout_id) or the
equivalent store method) and change get_payout_by_tx_hash to run a direct
indexed query (e.g.
session.query(Payout).filter_by(tx_hash=tx_hash).one_or_none() or the equivalent
store method) instead of iterating over pg_store.load_payouts; return
_payout_to_response(...) when a row is found and preserve the None return when
not found.
- Around line 243-259: The duplicate-check using load_buybacks(limit=1000) is
insufficient; wrap the call to pg_store.persist_buyback(record) in a try/except
that catches the DB unique-constraint/IntegrityError (or the specific DB driver
unique-violation exception) and re-raise HTTPException(status_code=400,
detail="Buyback already exists"); only add the record to _buyback_store under
_lock after persist_buyback succeeds (so use BuybackRecord,
pg_store.persist_buyback, BuybackTable.tx_hash, _buyback_store, and _lock to
locate and update the code).

In `@backend/requirements.txt`:
- Around line 27-29: The three new dependency lines for anthropic, openai, and
psutil in backend/requirements.txt lack upper bounds and must be changed to
match the project's bounded-range style; update each entry (anthropic, openai,
psutil) to use a capped range like ">=current_min,<next_major" (or
"<=latest_patch" if the file uses minor/patch caps) so upgrades cannot jump
major versions silently—identify the current minimum versions you added and set
the corresponding upper-bound cap consistent with other entries in
requirements.txt, then run the same PyPI/version check used in the review to
verify the chosen caps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d7fc91ab-c701-406d-bcb1-b171db510d65

📥 Commits

Reviewing files that changed from the base of the PR and between 3e496e5 and d876193.

📒 Files selected for processing (11)
  • backend/app/api/bounties.py
  • backend/app/api/payouts.py
  • backend/app/main.py
  • backend/app/middleware/security.py
  • backend/app/models/bounty.py
  • backend/app/models/bounty_table.py
  • backend/app/services/bounty_search_service.py
  • backend/app/services/payout_service.py
  • backend/app/services/pg_store.py
  • backend/requirements.txt
  • backend/tests/test_health.py

Comment on lines +155 to +163
@router.post("/validate-wallet", response_model=WalletValidationResponse)
async def validate_wallet(body: WalletValidationRequest) -> WalletValidationResponse:
"""Check base-58 format and reject known program addresses.

Returns a structured response indicating whether the address is
valid for receiving payouts.
"""
address = body.wallet_address
is_program = address in KNOWN_PROGRAM_ADDRESSES
try:
validate_solana_wallet(address)
return WalletValidationResponse(
wallet_address=address,
valid=True,
message="Valid Solana wallet address",
)
return WalletValidationResponse(wallet_address=address, valid=True, message="Valid Solana wallet address")
except ValueError as exc:
return WalletValidationResponse(
wallet_address=address,
valid=False,
is_program_address=is_program,
message=str(exc),
)
return WalletValidationResponse(wallet_address=address, valid=False, is_program_address=is_program, message=str(exc))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Valid program accounts lose the is_program_address flag.

validate_wallet() computes is_program, but the success response drops it. A known program address that passes base58 validation comes back as an ordinary valid wallet, so downstream callers lose the signal they need to block non-user payout targets.

As per coding guidelines, backend/**: "Analyze thoroughly: API contract consistency with spec".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/payouts.py` around lines 155 - 163, The success branch of
validate_wallet currently omits the computed is_program flag so known program
addresses are returned as ordinary valid wallets; update the success return in
validate_wallet (and ensure both branches use the same response shape) to
include is_program_address=is_program in the WalletValidationResponse so callers
can detect program accounts; reference validate_wallet and
WalletValidationResponse to locate and modify the response construction.

Comment on lines 136 to +161
app.add_middleware(LoggingMiddleware)

# Layer 4: Input sanitization — blocks XSS and SQL injection patterns
app.add_middleware(InputSanitizationMiddleware)

# Layer 3: Redis-backed token bucket rate limiter (upstream)
app.add_middleware(RateLimiterMiddleware)

# Layer 2: IP blocklist — blocks banned IPs via Redis set
app.add_middleware(RateLimitMiddleware)
app.add_middleware(IPBlocklistMiddleware)

# Layer 1 (outermost): Security headers — HSTS, CSP, X-Frame-Options, etc.
app.add_middleware(SecurityHeadersMiddleware)

@app.middleware("http")
async def add_request_id_and_timing(request: Request, call_next: Callable):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
request.state.request_id = request_id

start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time

response.headers["X-Request-ID"] = request_id
response.headers["X-Process-Time"] = str(process_time)

monitor.track_request(
path=request.url.path,
method=request.method,
status_code=response.status_code,
duration=process_time
)
return response
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Two middlewares now mint independent request IDs.

LoggingMiddleware already sets request.state.request_id and X-Request-ID, and the new add_request_id_and_timing middleware generates another value from the incoming header instead of reusing the first one. That makes correlation order-dependent and can leave logs, error payloads, and response headers referring to different IDs for the same request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/main.py` around lines 136 - 161, The add_request_id_and_timing
middleware must reuse any request ID already set by prior middleware
(LoggingMiddleware) instead of minting a new one; modify
add_request_id_and_timing to first check request.state.request_id and use it if
present, otherwise fall back to request.headers.get("X-Request-ID") or generate
uuid.uuid4(), then assign that value to request.state.request_id and set
response.headers["X-Request-ID"] (but only if not already set) so
LoggingMiddleware, add_request_id_and_timing, monitor.track_request, and any
error payloads all reference the same ID.

Comment on lines 138 to 140
app.add_middleware(RateLimiterMiddleware)

# Layer 2: IP blocklist — blocks banned IPs via Redis set
app.add_middleware(RateLimitMiddleware)
app.add_middleware(IPBlocklistMiddleware)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

/api/v2/health is now behind the blocking middlewares.

The route moved to /api/v2, but backend/app/middleware/ip_blocklist.py, backend/app/middleware/rate_limit.py, and backend/app/middleware/rate_limiter.py still only exempt /health or /api/health. Health probes can now be 403/429'd, and because both rate-limit middlewares are mounted here the failure mode is doubled.

As per coding guidelines, backend/**: "Analyze thoroughly: API contract consistency with spec".

Also applies to: 205-206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/main.py` around lines 138 - 140, The health-check endpoint moved
to /api/v2/health but the middleware exemptions in the IPBlocklistMiddleware,
RateLimitMiddleware, and RateLimiterMiddleware still only whitelist /health or
/api/health, causing probes to be blocked; update each middleware's exemption
logic (in the classes IPBlocklistMiddleware, RateLimitMiddleware, and
RateLimiterMiddleware) to also treat "/api/v2/health" as a permitted path (and
preserve the existing /health and /api/health checks), ensuring the middlewares
skip blocking/rate-limiting for that exact route so health probes are not
403/429'd.

Comment on lines +190 to +208
app.include_router(auth_router, prefix="/api", tags=["auth"])
app.include_router(bounties_router, prefix="/api", tags=["bounties"])
app.include_router(notifications_router, prefix="/api", tags=["notifications"])
app.include_router(leaderboard_router, prefix="/api", tags=["leaderboard"])
app.include_router(payouts_router, prefix="/api", tags=["payouts"])
app.include_router(buybacks.router, prefix="/api/buybacks", tags=["treasury"])
app.include_router(github_webhook_router, prefix="/api/webhooks", tags=["webhooks"])

# WebSocket: /ws/*
app.include_router(websocket_router)

# Agents: /api/agents/*
app.include_router(agents_router, prefix="/api")

# Disputes: /api/disputes/*
app.include_router(disputes_router, prefix="/api")

# Escrow: /api/escrow/*
app.include_router(escrow_router, prefix="/api")

# Stats: /api/stats (public endpoint)
app.include_router(stats_router, prefix="/api")

# Open Graph previews: /og/*
app.include_router(og_router)
app.include_router(websocket_router, tags=["websocket"])
app.include_router(agents_router, prefix="/api", tags=["agents"])
app.include_router(disputes_router, prefix="/api", tags=["disputes"])
app.include_router(escrow_router, prefix="/api", tags=["escrow"])
app.include_router(stats_router, prefix="/api", tags=["stats"])
app.include_router(og_router, tags=["og"])
app.include_router(contributor_webhooks_router, prefix="/api")
app.include_router(siws_router, prefix="/api")

# System Health: /health, Prometheus: /metrics
app.include_router(health_router)
app.include_router(metrics_router)

# Admin Dashboard: /api/admin/* (protected by ADMIN_API_KEY)
app.include_router(admin_router)


@app.post("/api/sync", tags=["admin"])
async def trigger_sync():
"""Manually trigger a GitHub to bounty and leaderboard sync.

This endpoint should be protected by admin authentication in production.
It forces an immediate resync of all bounty and contributor data from
the GitHub Issues API.

Returns:
dict: Sync results including counts of updated bounties and contributors.
"""
result = await sync_all()
return result
app.include_router(health_router, prefix="/api/v2", tags=["system"])
app.include_router(metrics_router, tags=["system"])
app.include_router(admin_router, tags=["admin"])

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The contributor API is no longer registered.

contributors_router is imported at Line 32, but the new registration block never includes it. That removes those endpoints from the running app even though the module still ships.

As per coding guidelines, backend/**: "Analyze thoroughly: API contract consistency with spec".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/main.py` around lines 190 - 208, The contributors_router
endpoints were never included in the app registration block; add
app.include_router(contributors_router, prefix="/api", tags=["contributors"]) to
the router registration sequence in main.py (near the other app.include_router
calls such as auth_router, bounties_router, and contributor_webhooks_router) so
the imported contributors_router is actually mounted and its endpoints become
available at the /api prefix.

Comment on lines +272 to +282
async def hydrate_from_database() -> None:
"""Startup hydration from PostgreSQL into memory caches."""
try:
payouts = await pg_store.load_payouts(limit=5000)
buybacks = await pg_store.load_buybacks(limit=1000)
with _lock:
_payout_store.update(payouts)
_buyback_store.update(buybacks)
log.info("Hydrated %d payouts and %d buybacks from PostgreSQL", len(payouts), len(buybacks))
except Exception as e:
log.error("Hydration failed: %s", e)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Startup hydration uses fixed limits, potentially missing data.

Lines 275-276 hydrate only 5000 payouts and 1000 buybacks. If the database contains more records, the in-memory cache will be incomplete from startup, causing inconsistent behavior between cache hits and DB fallbacks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 272 - 282, The startup
hydration in hydrate_from_database currently uses hardcoded limits
(pg_store.load_payouts(limit=5000), load_buybacks(limit=1000)) which can miss
records; change it to fetch all rows by paginating or streaming instead of a
single fixed-limit call: replace the single calls with a loop that repeatedly
calls pg_store.load_payouts(batch_size, offset/last_id) and
pg_store.load_buybacks(batch_size, offset/last_id) until no more rows are
returned, updating _payout_store and _buyback_store inside the existing _lock on
each batch, keep the final log.info to report total counts, and preserve the
try/except and log.error behavior in hydrate_from_database.

Comment on lines +16 to +19
def _to_uuid(val: Any) -> Any:
"""Coerce a string value to uuid.UUID for ORM lookups on UUID PK columns.

Args:
val: The value to coerce, typically a string UUID.

Returns:
A uuid.UUID instance if conversion succeeds, otherwise the original value.
"""
if isinstance(val, _uuid.UUID):
return val
try:
return _uuid.UUID(str(val))
except (ValueError, AttributeError):
return val


# ---------------------------------------------------------------------------
# Generic helpers
# ---------------------------------------------------------------------------


async def _upsert(
session: AsyncSession, model_cls: type, pk_value: Any, **columns: Any
) -> None:
"""Insert or update a row using merge (session-level upsert).
if isinstance(val, _uuid.UUID): return val
try: return _uuid.UUID(str(val))
except (ValueError, AttributeError): return val
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

_to_uuid silently returns invalid input on conversion failure.

Lines 18-19 catch ValueError/AttributeError and return the original value unchanged. If an invalid UUID string is passed, subsequent database operations expecting a UUID type will fail with confusing errors deeper in the stack, or worse, cause type mismatches.

Consider raising an explicit error or logging a warning when conversion fails for values that are expected to be UUIDs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 16 - 19, The helper _to_uuid
currently swallows ValueError/AttributeError and returns the original value on
failure; change it to surface conversion failures by raising a clear exception
(e.g., ValueError with the offending value and context) instead of returning the
original, or at minimum log a warning before returning; update the except block
in _to_uuid to raise a descriptive ValueError (or call the logger) so callers
know the UUID conversion failed.

Comment on lines 224 to +226
bounty_title=row.bounty_title,
tx_hash=row.tx_hash,
status=PayoutStatus(row.status),
solscan_url=row.solscan_url,
created_at=row.created_at,
)
log.info("Loaded %d payouts from PostgreSQL", len(out))
return out


async def load_buybacks(*, offset: int = 0, limit: int = 10000) -> dict[str, Any]:
"""Load buyback records from PostgreSQL into a dict keyed by ID string.

Args:
offset: Number of rows to skip.
limit: Maximum rows to return.

Returns:
Dict mapping buyback ID strings to BuybackRecord instances.
"""
from app.models.payout import BuybackRecord
from app.models.tables import BuybackTable

out: dict[str, Any] = {}
async with get_db_session() as session:
stmt = (
select(BuybackTable)
.order_by(BuybackTable.created_at.desc())
.offset(offset)
.limit(limit)
)
for row in (await session.execute(stmt)).scalars():
out[str(row.id)] = BuybackRecord(
id=str(row.id),
amount_sol=float(row.amount_sol),
amount_fndry=float(row.amount_fndry),
price_per_fndry=float(row.price_per_fndry),
tx_hash=row.tx_hash,
status=PayoutStatus(row.status.lower()),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Status parsing assumes lowercase format.

Line 226 calls PayoutStatus(row.status.lower()). If the database contains an unexpected status value (e.g., mixed case not matching enum values after lowercasing, or a completely invalid value), this will raise a ValueError and crash the entire load operation.

Consider wrapping in a try/except with logging for invalid statuses, or validating/normalizing status values during persistence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 224 - 226, The code currently
constructs PayoutStatus(row.status.lower()) which will raise ValueError for
unexpected values; wrap that conversion in a try/except ValueError, log the
invalid status along with identifying fields from the row (e.g., row.tx_hash or
row.id), and assign a safe fallback (use PayoutStatus.UNKNOWN if that enum
member exists, otherwise assign None or a designated default) so the load
operation doesn't crash; also consider adding a note to validate/normalize
status values when persisting.

Comment on lines +277 to 281
async def save_last_sync(dt: datetime) -> None:
from app.models.tables import SyncStateTable
async with get_db_session() as session:
await _insert_if_absent(
session,
ReputationHistoryTable,
entry.entry_id,
contributor_id=entry.contributor_id,
bounty_id=entry.bounty_id,
bounty_title=entry.bounty_title,
bounty_tier=entry.bounty_tier,
review_score=entry.review_score,
earned_reputation=entry.earned_reputation,
anti_farming_applied=entry.anti_farming_applied,
created_at=entry.created_at,
)
session.add(SyncStateTable(last_sync=dt))
await session.commit()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

save_last_sync always inserts a new row.

Line 280 unconditionally adds a new SyncStateTable row on every call. Over time, this creates unbounded growth in the sync_state table. Consider using upsert with a single known key, or deleting old rows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 277 - 281, The save_last_sync
function currently always inserts a new SyncStateTable row
(session.add(SyncStateTable(last_sync=dt))) causing unbounded growth; change it
to perform an upsert/update instead: locate the existing SyncStateTable row (by
its single known key/unique constraint or primary id) and update its last_sync
value, or use your ORM's upsert support (e.g., insert().on_conflict_do_update or
session.merge) to atomically set last_sync = dt, then commit; update the code
paths inside save_last_sync and keep using get_db_session() for the transaction.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/tests/e2e/conftest.py (1)

38-48: ⚠️ Potential issue | 🟡 Minor

Duplicate comment block.

The section header "Test user for dependency override" appears twice (lines 38-41 and 46-48).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/e2e/conftest.py` around lines 38 - 48, Remove the duplicated
section header comment block labeled "Test user for dependency override" so the
file only contains a single header; locate the two identical comment blocks
surrounding the imports (near the imports of UserResponse and DEFAULT_WALLET)
and delete the redundant one, keeping the single intended header and the imports
intact (references: UserResponse import and DEFAULT_WALLET import).
♻️ Duplicate comments (18)
backend/app/api/buybacks.py (2)

9-12: ⚠️ Potential issue | 🔴 Critical

record_buyback is still unauthenticated for a treasury-mutating operation.

Line 10 exposes a state-changing treasury endpoint without any authentication/authorization dependency, so any caller can submit buyback events.

As per coding guidelines backend/**: Python FastAPI backend. Analyze thoroughly: Authentication/authorization gaps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/buybacks.py` around lines 9 - 12, The endpoint record_buyback
currently allows unauthenticated callers to mutate treasury state; require and
enforce authentication/authorization by adding a FastAPI dependency to the
function signature (e.g., add a parameter like current_user: User =
Depends(get_current_user) or current_admin: User = Depends(require_admin)
depending on your auth utilities) and validate admin/treasury scope before
calling payout_service.create_buyback; update the router declaration/signature
in backend/app/api/buybacks.py (record_buyback) to use the appropriate Depends
helper (get_current_user, require_admin, or require_scope) and return 403 on
insufficient privileges.

15-17: ⚠️ Potential issue | 🟠 Major

Pagination parameters remain unbounded at the API layer.

Line 15 accepts unconstrained skip and limit. Extremely large values can be used to force excessive DB/data processing.

As per coding guidelines backend/**: Python FastAPI backend. Analyze thoroughly: Input validation and SQL injection vectors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/buybacks.py` around lines 15 - 17, The list_buybacks endpoint
accepts unconstrained skip and limit which allows very large values; validate
and clamp these inputs in the API layer (function list_buybacks) before calling
payout_service.list_buybacks: ensure skip is an integer >= 0 and limit is an
integer within an allowed maximum (e.g. <= 1000) and a sensible default (e.g.
100), returning a 400 on invalid input or silently clamping values; implement
this using FastAPI Query parameter constraints or explicit runtime checks and
pass only the sanitized values to payout_service.list_buybacks to prevent
excessive DB load and potential abuse.
backend/app/middleware/rate_limit.py (2)

59-64: ⚠️ Potential issue | 🔴 Critical

Client identity is still spoofable via X-Forwarded-For.

Lines 61-64 trust the first XFF hop directly, allowing callers to rotate identities and bypass bucket enforcement.

As per coding guidelines backend/**: Python FastAPI backend. Analyze thoroughly: Authentication/authorization gaps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/middleware/rate_limit.py` around lines 59 - 64, The current
_get_client_id trusts the first value in the X-Forwarded-For header which is
easily spoofed; change it to only honor X-Forwarded-For when the immediate peer
(request.client.host) is a configured trusted proxy, otherwise ignore XFF and
use request.client.host; when honoring XFF, take the right-most untrusted IP (or
the left-most if your infra documents it) from XFF consistently and
document/configure trusted proxies via a TRUSTED_PROXIES config/env used by
_get_client_id to validate request.client.host before parsing XFF.

94-102: ⚠️ Potential issue | 🟠 Major

Rate-limit response headers do not match enforced bucket semantics.

Lines 95 and 101 always advertise limit 2 while the bucket is configured with burst=10 and refill behavior from Line 66; Retry-After is also hardcoded, so clients receive inconsistent throttling metadata.

As per coding guidelines backend/**: Python FastAPI backend. Analyze thoroughly: API contract consistency with spec.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/middleware/rate_limit.py` around lines 94 - 102, The response
headers in the rate-limiting middleware are hardcoded and inconsistent with the
token bucket config: replace the fixed "2" and hardcoded "Retry-After" with
values derived from the actual bucket settings (use the configured burst
capacity and current token count) and compute Retry-After from the token
refill/reset time; specifically, in the code paths around call_next(request)
where response.headers and remaining are set, set "X-RateLimit-Limit" to the
bucket's burst capacity, "X-RateLimit-Remaining" to the current remaining tokens
(the existing remaining variable), and set "Retry-After" to the calculated
seconds until a token becomes available based on the refill/reset logic used
earlier (referencing the burst and refill/reset variables used in the token
bucket implementation).
backend/app/services/health.py (1)

34-56: ⚠️ Potential issue | 🟠 Major

Request tracking still has TOCTOU and unbounded cardinality risks.

Line 36 reads _running outside the lock, while updates happen under lock, which permits racey accounting. Also Line 42 stores raw path keys without bounds/normalization, so high-cardinality paths can grow memory unbounded and degrade Line 55 sorting cost.

As per coding guidelines backend/**: Python FastAPI backend. Analyze thoroughly: Error handling and edge case coverage.

backend/logs/application.log.2026-03-21 (1)

1-15: ⚠️ Potential issue | 🟠 Major

This is another committed runtime log artifact with request metadata.

Lines 1-15 include request/trace telemetry (request IDs, paths, client IP, timings) that should not live in version-controlled source.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/logs/application.log.2026-03-21` around lines 1 - 15, The committed
runtime log file application.log.2026-03-21 contains request/trace telemetry and
must be removed from the repo; remove it from version control (git rm --cached
application.log.2026-03-21 and commit), add a rule to .gitignore to prevent
re-adding logs, and if any sensitive data may have been exposed rewrite history
using git filter-repo or BFG (or coordinate with the security team) to purge the
file from prior commits; finally push the cleaned branch and notify reviewers
that the log file was removed and .gitignore updated.
backend/app/main.py (4)

190-207: ⚠️ Potential issue | 🟠 Major

contributors_router is imported but never registered.

Line 32 imports contributors_router from app.api.bounties, but the router registration block (lines 190-207) does not include it. This removes contributor endpoints from the API.

 app.include_router(auth_router, prefix="/api", tags=["auth"])
 app.include_router(bounties_router, prefix="/api", tags=["bounties"])
+app.include_router(contributors_router, prefix="/api", tags=["contributors"])
 app.include_router(notifications_router, prefix="/api", tags=["notifications"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/main.py` around lines 190 - 207, The contributors_router imported
as contributors_router (from app.api.bounties) is not registered with the
FastAPI app; add a registration line in the router block using
app.include_router(contributors_router, prefix="/api", tags=["contributors"])
(place it alongside the other app.include_router calls near
auth_router/bounties_router) so contributor endpoints are exposed.

143-161: ⚠️ Potential issue | 🟠 Major

Duplicate request ID generation with LoggingMiddleware.

Both LoggingMiddleware (see backend/app/middleware/logging_middleware.py lines 14-22) and add_request_id_and_timing generate and set X-Request-ID. The middleware execution order means LoggingMiddleware runs first (added earlier), sets request.state.request_id, then add_request_id_and_timing overwrites it with a new value from the header or a fresh UUID, causing log correlation mismatches.

Consolidate request ID handling into one location:

 `@app.middleware`("http")
 async def add_request_id_and_timing(request: Request, call_next: Callable):
-    request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
+    # Reuse ID from LoggingMiddleware if already set
+    request_id = getattr(request.state, "request_id", None) or request.headers.get("X-Request-ID") or str(uuid.uuid4())
     request.state.request_id = request_id
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/main.py` around lines 143 - 161, add_request_id_and_timing is
overwriting the request ID that LoggingMiddleware already sets; update
add_request_id_and_timing to respect an existing request.state.request_id (use
that if present) and only fall back to request.headers.get("X-Request-ID") or a
new uuid when request.state.request_id is missing, then set
response.headers["X-Request-ID"] to that chosen ID and use it in
monitor.track_request; this consolidates request ID handling so
LoggingMiddleware remains the single source of truth and prevents log
correlation issues.

205-206: ⚠️ Potential issue | 🟠 Major

Health endpoint at /api/v2/health is not exempt from rate limiting.

The health router is mounted at /api/v2 (line 205), making the health check available at /api/v2/health. However, RateLimitMiddleware and RateLimiterMiddleware only exempt /health and /api/health paths. Health probes from load balancers and orchestrators may be rate-limited or blocked.

#!/bin/bash
# Check rate limiter exempt paths
rg -n "EXEMPT_PATHS|/health" backend/app/middleware/rate_limit*.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/main.py` around lines 205 - 206, The health endpoint is mounted
via health_router at the "/api/v2" prefix (health accessible at
"/api/v2/health") but the rate limiters only exempt "/health" and "/api/health";
update the rate limiting exemption logic by adding "/api/v2/health" (or better:
canonicalize and include any router prefix + "/health") to the EXEMPT_PATHS used
by RateLimitMiddleware and RateLimiterMiddleware, or change their path check to
allow any path that endswith "/health" (or startswith any registered health
prefix) so requests to health_router (health_router, RateLimitMiddleware,
RateLimiterMiddleware, EXEMPT_PATHS) are not rate limited.

128-134: ⚠️ Potential issue | 🟠 Major

CORS configuration violates Starlette/FastAPI specification.

Setting allow_credentials=True with allow_origins=["*"], allow_methods=["*"], and allow_headers=["*"] is explicitly prohibited by Starlette's CORSMiddleware. Per official documentation, when allow_credentials=True, none of these can use wildcards. This will cause authenticated cross-origin requests to fail.

 app.add_middleware(
     CORSMiddleware,
-    allow_origins=["*"],
-    allow_credentials=True,
-    allow_methods=["*"],
-    allow_headers=["*"],
+    allow_origins=["https://solfoundry.org", "http://localhost:3000"],  # Explicit origins
+    allow_credentials=True,
+    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
+    allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/main.py` around lines 128 - 134, The CORSMiddleware configuration
in app.add_middleware using CORSMiddleware sets allow_credentials=True while
using wildcards for origins/methods/headers, which violates Starlette/FastAPI
rules; either set allow_credentials=False if you need global wildcards, or
replace the wildcard origins/methods/headers with an explicit allow_origins list
(and explicit allow_methods/allow_headers if required) so authenticated requests
succeed. Update the CORSMiddleware call in the app.add_middleware block: choose
one of two fixes—(a) set allow_credentials=False to keep allow_origins=["*"],
allow_methods=["*"], allow_headers=["*"], or (b) provide a concrete list for
allow_origins (and non-wildcard allow_methods/allow_headers if desired) while
leaving allow_credentials=True—so the middleware conforms to Starlette/FastAPI
expectations.
backend/app/api/payouts.py (1)

154-162: ⚠️ Potential issue | 🟠 Major

Valid program accounts still lose the is_program_address flag on success.

The success path at line 160 omits is_program_address from the response, while the failure path at line 162 includes it. A known program address that passes base58 validation returns is_program_address=False (the default) instead of True, causing downstream callers to lose the signal needed to block non-user payout targets.

-        return WalletValidationResponse(wallet_address=address, valid=True, message="Valid Solana wallet address")
+        return WalletValidationResponse(wallet_address=address, valid=True, is_program_address=is_program, message="Valid Solana wallet address")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/payouts.py` around lines 154 - 162, In validate_wallet, the
success path currently omits the is_program_address flag so known program
addresses lose that signal; update the WalletValidationResponse returned in the
try block inside validate_wallet to include is_program_address=is_program
(keeping wallet_address, valid=True and message unchanged) so both success and
except paths consistently populate is_program_address; reference
validate_wallet, is_program, and WalletValidationResponse to locate and change
the return in the try block.
backend/app/services/payout_service.py (7)

135-153: ⚠️ Potential issue | 🟠 Major

Payout lookups truncate at 5000/10000 records.

get_payout_by_id (line 142) and get_payout_by_tx_hash (line 149) load limited records. Once the table exceeds this size, valid lookups return false 404s.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 135 - 153, The lookup
functions get_payout_by_id and get_payout_by_tx_hash currently call
pg_store.load_payouts with hard limits (limit=5000/10000) which truncates
results and causes false negatives; change them to perform targeted DB queries
instead of loading a limited batch: for get_payout_by_id call a
pg_store.load_payout_by_id(payout_id) (or equivalent single-row query) and for
get_payout_by_tx_hash call pg_store.load_payout_by_tx_hash(tx_hash), falling
back to the in-memory _payout_store only if those targeted queries return
nothing; update the service to rely on these new/single-row pg_store methods (or
modify load_payouts to accept no limit/streaming) so lookups are complete.

155-179: ⚠️ Potential issue | 🟡 Minor

approve_payout and reject_payout do not update in-memory cache after persist.

After persisting state changes to the database, neither function updates _payout_store. Subsequent reads from cache return stale status values.

     await pg_store.persist_payout(record)
+    with _store_lock:
+        _payout_store[record.id] = record

Also applies to: 181-197

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 155 - 179,
approve_payout and reject_payout update the DB but never refresh the in-memory
cache (_payout_store), causing stale reads; after calling
pg_store.persist_payout(record) in both approve_payout and reject_payout,
acquire _lock and update _payout_store[payout_id] = record (or set the status
and updated_at on the cached object if present) so the in-memory entry reflects
the persisted PayoutStatus and timestamp; ensure this uses the same _lock used
when reading the cache to avoid races and handle the case where the cache entry
didn't exist by inserting the record.

110-133: ⚠️ Potential issue | 🟠 Major

In-memory filtering with limit + skip + 1000 cap produces incorrect totals.

Line 110 fetches a bounded set, then line 126 sets total = len(items) from the filtered subset. For datasets exceeding this threshold, pagination metadata is wrong and records are missing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 110 - 133, The current
logic calls pg_store.load_payouts(limit=limit + skip + 1000) then computes total
= len(items) after doing in-memory filters, which causes incorrect totals and
missing records for large datasets; update the implementation so filtering and
counting happen at the data source (use pg_store.load_payouts with appropriate
filter parameters or add a new pg_store.count_payouts(filters) method) and then
fetch only the paginated rows (apply skip/limit at DB) before mapping with
_payout_to_response into PayoutListResponse, ensuring total reflects the full
count of filtered records and page contains only the requested slice.

261-270: ⚠️ Potential issue | 🟠 Major

list_buybacks reports incorrect total count.

Line 267 sets total=len(items) from the fetched subset rather than the true database count.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 261 - 270, The total
returned by list_buybacks is wrong because it uses total=len(items) after
paging; change it to compute the true total before slicing (e.g., all_items =
list(all_records.values()); total = len(all_items)) or call a dedicated count
method (e.g., pg_store.count_buybacks()) and use that value for
BuybackListResponse.total; update list_buybacks (function name) to set total to
the full count and keep paging logic (page = all_items[skip: skip+limit]) and
then return items from the page.

279-284: ⚠️ Potential issue | 🟡 Minor

get_total_paid_out truncates at 10000 records.

For systems with more than 10000 confirmed payouts, this returns an understated total.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 279 - 284,
get_total_paid_out currently calls pg_store.load_payouts(limit=10000) which
truncates results and undercounts totals; update get_total_paid_out to fetch all
payouts (either by removing the hard limit if load_payouts supports unlimited,
or implement pagination/streaming using pg_store.load_payouts in a loop with
offset/limit until no more rows) and then sum PayoutStatus.CONFIRMED amounts for
token == "FNDRY" and token == "SOL" (keep existing variables fndry and sol and
the filtering logic). Ensure the implementation avoids loading an unbounded
result into memory at once by using a reasonable page size and aggregating per
page.

242-259: ⚠️ Potential issue | 🟠 Major

Buyback duplicate check has same race/truncation issues.

Line 244 loads only 1000 buybacks. The UNIQUE constraint on BuybackTable.tx_hash will throw an unhandled database error for duplicates outside this window or during races, resulting in 500 errors instead of controlled 400s.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 242 - 259, The
duplicate-check using pg_store.load_buybacks(limit=1000) in create_buyback is
racy and truncated; replace it by either a single DB lookup for the given
tx_hash (e.g., add/use pg_store.get_buyback_by_tx_hash(tx_hash) and raise
HTTPException(400) if found) or keep a single insert but wrap the call to
pg_store.persist_buyback in a try/except that catches the DB unique-constraint
error (e.g., IntegrityError/UniqueViolation from your DB driver) and convert it
to HTTPException(status_code=400, detail="Buyback already exists"); after
successful persist, continue to update _buyback_store under _store_lock and
return _buyback_to_response(record).

61-94: ⚠️ Potential issue | 🔴 Critical

Double-pay check remains racy and limited to 5000 records.

The bounty-level duplicate check (lines 71-75) loads only 5000 payouts. This misses older payouts and the check-then-persist sequence is not atomic. Concurrent requests can race past the check. Without a database-level UNIQUE constraint on bounty_id, double payments remain possible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 61 - 94, create_payout
currently does an in-memory/dataset-level double-pay check (load_payouts with
limit=5000 and loop) which is racy and incomplete; instead add a DB-level UNIQUE
constraint/index on the payouts.bounty_id column and rely on the database for
atomic enforcement, update the persistence path to handle that constraint
violation: modify the schema/migration to add UNIQUE(bounty_id) (allowing nulls
if needed), update pg_store.persist_payout to catch the DB
IntegrityError/unique-violation for bounty_id and translate it into
HTTPException(status_code=400, detail="Bounty {bounty_id} already has a
payout."), and remove or keep but not rely on the current load_payouts/loop
check in create_payout so concurrent requests cannot create double payouts;
references: create_payout, load_payouts, pg_store.persist_payout, PayoutRecord,
bounty_id.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/payouts.py`:
- Around line 187-216: The execute_payout handler is using unnecessary hasattr
checks for attributes that always exist on PayoutResponse and is referencing a
non‑existent contributor_id; replace the hasattr usages by directly reading
fields from the PayoutResponse returned by process_payout (e.g. use
result.bounty_id, str(result.amount), result.tx_hash) and remove the
contributor_id logic entirely; if you need to fall back to payout_id when
bounty_id is explicitly None, do an explicit None check (bounty_id =
result.bounty_id if result.bounty_id is not None else payout_id) rather than
getattr(..., None) or truthy "or" fallbacks, and call
ContributorWebhookService.dispatch_event with user_id omitted or None.

In `@backend/app/middleware/rate_limit.py`:
- Around line 54-57: The Redis client in RateLimitMiddleware.__init__
initializes self.redis via redis.from_url(redis_url, decode_responses=True)
without socket timeouts; update that call to pass explicit
socket_connect_timeout and socket_timeout (e.g., short connect timeout and
reasonable read timeout) so socket operations fail fast during network
degradation, then retain the existing fail-open error handling around
_lua_script and request processing; apply the same change to other Redis
initializations that use redis.from_url in websocket_manager (WebsocketManager),
core/redis (module-level client factory), api/health (health check client), and
api/admin (admin Redis client) to ensure consistent timeout behavior across the
codebase.
- Around line 82-83: The health-check bypass currently only matches
"/api/health" and misses the registered "/api/v2/health"; update the check in
backend/app/middleware/rate_limit.py (the branch that returns await
call_next(request)) to also allow "/api/v2/health" — better: replace the
exact-path check with a more robust match (e.g.,
request.url.path.endswith("/health") or a contains "/health" rule) so any
prefixed health route passes through; also update the alternate
RateLimiterMiddleware exemption in backend/app/middleware/rate_limiter.py (the
logic around paths starting with "/health") to use the same robust matching
(endswith or wildcard) so both middleware implementations consistently bypass
all health endpoints.

In `@backend/app/middleware/security.py`:
- Around line 35-43: The CSP is currently too permissive globally via
CSP_SCRIPT_SRC and CSP_STYLE_SRC (they include 'unsafe-inline' and
'unsafe-eval') and is applied in the global middleware in security.py; change
the global CSP to remove 'unsafe-inline' and 'unsafe-eval' (tighten
CSP_DEFAULT_SRC/CSP_SCRIPT_SRC/CSP_STYLE_SRC/CSP_IMG_SRC/CSP_CONNECT_SRC/CSP_FONT_SRC)
and then disable FastAPI's default /docs and /redoc routes, creating a custom
/docs handler that injects per-request nonces into inline scripts/styles and
applies a relaxed CSP header only for that route; update the middleware that
sets the response header to use the strict env-based values by default and add
route-level logic to set the relaxed CSP when serving the custom docs endpoint.

In `@backend/app/services/payout_service.py`:
- Around line 65-68: The code in create_payout references an undefined lock
variable `_lock`, causing a NameError; replace `_lock` with the correctly
defined module-level lock `_store_lock` (or create an alias) so the critical
section protecting `_payout_store` uses the right synchronization primitive;
update the block that iterates `_payout_store.values()` (inside create_payout)
to use `_store_lock` to prevent race conditions when checking existing.tx_hash.

In `@backend/app/services/treasury_service.py`:
- Around line 16-20: The module redefines _count_confirmed_payouts and
_count_buybacks as synchronous functions, shadowing the async helpers imported
earlier and causing await _count_confirmed_payouts() / await _count_buybacks()
in get_treasury_stats to fail at runtime; fix by renaming the local synchronous
implementations (e.g., to _count_confirmed_payouts_sync and
_count_buybacks_sync) or otherwise removing the name collision so the async
helpers imported on lines where _count_confirmed_payouts and _count_buybacks are
brought into the module remain the callable symbols used by get_treasury_stats,
and update any internal calls that referenced the local names accordingly.

In `@backend/logs/error.log.2026-03-21`:
- Around line 1-2: This commit accidentally includes runtime error logs (entries
mentioning "RateLimiter", "Blocklist", "Failsafe: ALLOW", and the Starlette
taskName "starlette.middleware.base.BaseHTTPMiddleware.__call__...coro"); remove
that log file from the repo, add the log filename pattern (e.g., error.log.* or
the specific runtime log name) to .gitignore, and revert the file from the
commit history so operational logs are not tracked; additionally, ensure your
logging config writes to a non-source-control sink (rotating file, syslog, or
cloud logs) and add a pre-commit/CI check to prevent committed runtime logs in
the future.

In `@backend/tests/e2e/conftest.py`:
- Around line 85-93: The current _get_test_loop function and module-level
_test_loop global create shared mutable event loop state causing test pollution;
replace this pattern with a pytest fixture that creates a fresh event loop per
test instead of using _test_loop/_get_test_loop. Implement a fixture (e.g.,
async def or regular def) that calls asyncio.new_event_loop(), sets it via
asyncio.set_event_loop(loop), yields the loop to the test, then on teardown
closes the loop and clears the event loop (asyncio.set_event_loop(None)) to
ensure no stale state; remove the _test_loop global and any callers of
_get_test_loop (or update callers to depend on the fixture) so tests use
pytest-asyncio/fixture-managed loops.
- Around line 29-30: The test hard-codes TEST_DB_PATH causing non-portable
tests; change conftest.py to create a temporary database path instead of the
absolute path by replacing TEST_DB_PATH and the os.environ["DATABASE_URL"]
assignment with a portable approach (e.g., use
tempfile.mkstemp()/tempfile.NamedTemporaryFile or pytest's tmp_path fixture) to
generate a temp file path and set os.environ["DATABASE_URL"] to
f"sqlite+aiosqlite:///{temp_db_path}" so tests run on CI and other machines.
- Around line 99-125: The module-level call inside _create_test_app() that runs
_get_test_loop().run_until_complete(_async_init()) performs schema creation
(using engine and Base) during import and forces asyncio.set_event_loop(), which
conflicts with pytest-asyncio; move the schema creation out of module import and
into a session-scoped pytest fixture (or perform lazy initialization) so that
_async_init() is awaited within a fixture using the pytest event loop instead of
calling _get_test_loop() at import time; update references to
_create_test_app(), _async_init(), engine and Base so schema setup runs inside
that session fixture before tests start.

---

Outside diff comments:
In `@backend/tests/e2e/conftest.py`:
- Around line 38-48: Remove the duplicated section header comment block labeled
"Test user for dependency override" so the file only contains a single header;
locate the two identical comment blocks surrounding the imports (near the
imports of UserResponse and DEFAULT_WALLET) and delete the redundant one,
keeping the single intended header and the imports intact (references:
UserResponse import and DEFAULT_WALLET import).

---

Duplicate comments:
In `@backend/app/api/buybacks.py`:
- Around line 9-12: The endpoint record_buyback currently allows unauthenticated
callers to mutate treasury state; require and enforce
authentication/authorization by adding a FastAPI dependency to the function
signature (e.g., add a parameter like current_user: User =
Depends(get_current_user) or current_admin: User = Depends(require_admin)
depending on your auth utilities) and validate admin/treasury scope before
calling payout_service.create_buyback; update the router declaration/signature
in backend/app/api/buybacks.py (record_buyback) to use the appropriate Depends
helper (get_current_user, require_admin, or require_scope) and return 403 on
insufficient privileges.
- Around line 15-17: The list_buybacks endpoint accepts unconstrained skip and
limit which allows very large values; validate and clamp these inputs in the API
layer (function list_buybacks) before calling payout_service.list_buybacks:
ensure skip is an integer >= 0 and limit is an integer within an allowed maximum
(e.g. <= 1000) and a sensible default (e.g. 100), returning a 400 on invalid
input or silently clamping values; implement this using FastAPI Query parameter
constraints or explicit runtime checks and pass only the sanitized values to
payout_service.list_buybacks to prevent excessive DB load and potential abuse.

In `@backend/app/api/payouts.py`:
- Around line 154-162: In validate_wallet, the success path currently omits the
is_program_address flag so known program addresses lose that signal; update the
WalletValidationResponse returned in the try block inside validate_wallet to
include is_program_address=is_program (keeping wallet_address, valid=True and
message unchanged) so both success and except paths consistently populate
is_program_address; reference validate_wallet, is_program, and
WalletValidationResponse to locate and change the return in the try block.

In `@backend/app/main.py`:
- Around line 190-207: The contributors_router imported as contributors_router
(from app.api.bounties) is not registered with the FastAPI app; add a
registration line in the router block using
app.include_router(contributors_router, prefix="/api", tags=["contributors"])
(place it alongside the other app.include_router calls near
auth_router/bounties_router) so contributor endpoints are exposed.
- Around line 143-161: add_request_id_and_timing is overwriting the request ID
that LoggingMiddleware already sets; update add_request_id_and_timing to respect
an existing request.state.request_id (use that if present) and only fall back to
request.headers.get("X-Request-ID") or a new uuid when request.state.request_id
is missing, then set response.headers["X-Request-ID"] to that chosen ID and use
it in monitor.track_request; this consolidates request ID handling so
LoggingMiddleware remains the single source of truth and prevents log
correlation issues.
- Around line 205-206: The health endpoint is mounted via health_router at the
"/api/v2" prefix (health accessible at "/api/v2/health") but the rate limiters
only exempt "/health" and "/api/health"; update the rate limiting exemption
logic by adding "/api/v2/health" (or better: canonicalize and include any router
prefix + "/health") to the EXEMPT_PATHS used by RateLimitMiddleware and
RateLimiterMiddleware, or change their path check to allow any path that
endswith "/health" (or startswith any registered health prefix) so requests to
health_router (health_router, RateLimitMiddleware, RateLimiterMiddleware,
EXEMPT_PATHS) are not rate limited.
- Around line 128-134: The CORSMiddleware configuration in app.add_middleware
using CORSMiddleware sets allow_credentials=True while using wildcards for
origins/methods/headers, which violates Starlette/FastAPI rules; either set
allow_credentials=False if you need global wildcards, or replace the wildcard
origins/methods/headers with an explicit allow_origins list (and explicit
allow_methods/allow_headers if required) so authenticated requests succeed.
Update the CORSMiddleware call in the app.add_middleware block: choose one of
two fixes—(a) set allow_credentials=False to keep allow_origins=["*"],
allow_methods=["*"], allow_headers=["*"], or (b) provide a concrete list for
allow_origins (and non-wildcard allow_methods/allow_headers if desired) while
leaving allow_credentials=True—so the middleware conforms to Starlette/FastAPI
expectations.

In `@backend/app/middleware/rate_limit.py`:
- Around line 59-64: The current _get_client_id trusts the first value in the
X-Forwarded-For header which is easily spoofed; change it to only honor
X-Forwarded-For when the immediate peer (request.client.host) is a configured
trusted proxy, otherwise ignore XFF and use request.client.host; when honoring
XFF, take the right-most untrusted IP (or the left-most if your infra documents
it) from XFF consistently and document/configure trusted proxies via a
TRUSTED_PROXIES config/env used by _get_client_id to validate
request.client.host before parsing XFF.
- Around line 94-102: The response headers in the rate-limiting middleware are
hardcoded and inconsistent with the token bucket config: replace the fixed "2"
and hardcoded "Retry-After" with values derived from the actual bucket settings
(use the configured burst capacity and current token count) and compute
Retry-After from the token refill/reset time; specifically, in the code paths
around call_next(request) where response.headers and remaining are set, set
"X-RateLimit-Limit" to the bucket's burst capacity, "X-RateLimit-Remaining" to
the current remaining tokens (the existing remaining variable), and set
"Retry-After" to the calculated seconds until a token becomes available based on
the refill/reset logic used earlier (referencing the burst and refill/reset
variables used in the token bucket implementation).

In `@backend/app/services/payout_service.py`:
- Around line 135-153: The lookup functions get_payout_by_id and
get_payout_by_tx_hash currently call pg_store.load_payouts with hard limits
(limit=5000/10000) which truncates results and causes false negatives; change
them to perform targeted DB queries instead of loading a limited batch: for
get_payout_by_id call a pg_store.load_payout_by_id(payout_id) (or equivalent
single-row query) and for get_payout_by_tx_hash call
pg_store.load_payout_by_tx_hash(tx_hash), falling back to the in-memory
_payout_store only if those targeted queries return nothing; update the service
to rely on these new/single-row pg_store methods (or modify load_payouts to
accept no limit/streaming) so lookups are complete.
- Around line 155-179: approve_payout and reject_payout update the DB but never
refresh the in-memory cache (_payout_store), causing stale reads; after calling
pg_store.persist_payout(record) in both approve_payout and reject_payout,
acquire _lock and update _payout_store[payout_id] = record (or set the status
and updated_at on the cached object if present) so the in-memory entry reflects
the persisted PayoutStatus and timestamp; ensure this uses the same _lock used
when reading the cache to avoid races and handle the case where the cache entry
didn't exist by inserting the record.
- Around line 110-133: The current logic calls pg_store.load_payouts(limit=limit
+ skip + 1000) then computes total = len(items) after doing in-memory filters,
which causes incorrect totals and missing records for large datasets; update the
implementation so filtering and counting happen at the data source (use
pg_store.load_payouts with appropriate filter parameters or add a new
pg_store.count_payouts(filters) method) and then fetch only the paginated rows
(apply skip/limit at DB) before mapping with _payout_to_response into
PayoutListResponse, ensuring total reflects the full count of filtered records
and page contains only the requested slice.
- Around line 261-270: The total returned by list_buybacks is wrong because it
uses total=len(items) after paging; change it to compute the true total before
slicing (e.g., all_items = list(all_records.values()); total = len(all_items))
or call a dedicated count method (e.g., pg_store.count_buybacks()) and use that
value for BuybackListResponse.total; update list_buybacks (function name) to set
total to the full count and keep paging logic (page = all_items[skip:
skip+limit]) and then return items from the page.
- Around line 279-284: get_total_paid_out currently calls
pg_store.load_payouts(limit=10000) which truncates results and undercounts
totals; update get_total_paid_out to fetch all payouts (either by removing the
hard limit if load_payouts supports unlimited, or implement pagination/streaming
using pg_store.load_payouts in a loop with offset/limit until no more rows) and
then sum PayoutStatus.CONFIRMED amounts for token == "FNDRY" and token == "SOL"
(keep existing variables fndry and sol and the filtering logic). Ensure the
implementation avoids loading an unbounded result into memory at once by using a
reasonable page size and aggregating per page.
- Around line 242-259: The duplicate-check using
pg_store.load_buybacks(limit=1000) in create_buyback is racy and truncated;
replace it by either a single DB lookup for the given tx_hash (e.g., add/use
pg_store.get_buyback_by_tx_hash(tx_hash) and raise HTTPException(400) if found)
or keep a single insert but wrap the call to pg_store.persist_buyback in a
try/except that catches the DB unique-constraint error (e.g.,
IntegrityError/UniqueViolation from your DB driver) and convert it to
HTTPException(status_code=400, detail="Buyback already exists"); after
successful persist, continue to update _buyback_store under _store_lock and
return _buyback_to_response(record).
- Around line 61-94: create_payout currently does an in-memory/dataset-level
double-pay check (load_payouts with limit=5000 and loop) which is racy and
incomplete; instead add a DB-level UNIQUE constraint/index on the
payouts.bounty_id column and rely on the database for atomic enforcement, update
the persistence path to handle that constraint violation: modify the
schema/migration to add UNIQUE(bounty_id) (allowing nulls if needed), update
pg_store.persist_payout to catch the DB IntegrityError/unique-violation for
bounty_id and translate it into HTTPException(status_code=400, detail="Bounty
{bounty_id} already has a payout."), and remove or keep but not rely on the
current load_payouts/loop check in create_payout so concurrent requests cannot
create double payouts; references: create_payout, load_payouts,
pg_store.persist_payout, PayoutRecord, bounty_id.

In `@backend/logs/application.log.2026-03-21`:
- Around line 1-15: The committed runtime log file application.log.2026-03-21
contains request/trace telemetry and must be removed from the repo; remove it
from version control (git rm --cached application.log.2026-03-21 and commit),
add a rule to .gitignore to prevent re-adding logs, and if any sensitive data
may have been exposed rewrite history using git filter-repo or BFG (or
coordinate with the security team) to purge the file from prior commits; finally
push the cleaned branch and notify reviewers that the log file was removed and
.gitignore updated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 909726b6-e8cd-4c1e-98cc-832cb02eb893

📥 Commits

Reviewing files that changed from the base of the PR and between d876193 and 60f63e3.

📒 Files selected for processing (16)
  • backend/app/api/buybacks.py
  • backend/app/api/payouts.py
  • backend/app/database.py
  • backend/app/main.py
  • backend/app/middleware/rate_limit.py
  • backend/app/middleware/security.py
  • backend/app/models/bounty.py
  • backend/app/services/auth_service.py
  • backend/app/services/bounty_service.py
  • backend/app/services/health.py
  • backend/app/services/payout_service.py
  • backend/app/services/treasury_service.py
  • backend/logs/application.log.2026-03-21
  • backend/logs/audit.log.2026-03-21
  • backend/logs/error.log.2026-03-21
  • backend/tests/e2e/conftest.py

Comment on lines +187 to 216
@router.post("/{payout_id}/execute", response_model=PayoutResponse)
async def execute_payout(payout_id: str, db: AsyncSession = Depends(get_db)) -> PayoutResponse:
try:
result = await process_payout(payout_id)
invalidate_cache()

# Notify contributor webhooks: bounty paid
try:
wh_service = ContributorWebhookService(db)
bounty_id = result.bounty_id if hasattr(result, "bounty_id") else payout_id
contributor_id = result.contributor_id if hasattr(result, "contributor_id") else None
await wh_service.dispatch_event(
"bounty.paid",
str(bounty_id),
{
"payout_id": payout_id,
"amount": str(result.amount) if hasattr(result, "amount") else None,
"tx_hash": result.tx_hash if hasattr(result, "tx_hash") else None,
},
user_id=str(contributor_id) if contributor_id else None,
)
except Exception as e:
logger.error("Failed to dispatch payout webhook: %s", e)

return result

except PayoutNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except InvalidPayoutTransitionError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check PayoutResponse fields
ast-grep --pattern 'class PayoutResponse($$$) {
  $$$
}'
rg -n "class PayoutResponse" -A 30 backend/app/models/payout.py

Repository: SolFoundry/solfoundry

Length of output: 1682


🏁 Script executed:

#!/bin/bash
# Find process_payout function definition
rg -n "def process_payout\|async def process_payout" backend/ -A 15

# Also search for where ContributorWebhookService.dispatch_event is defined
rg -n "class ContributorWebhookService" backend/ -A 30 | head -50

Repository: SolFoundry/solfoundry

Length of output: 2852


🏁 Script executed:

#!/bin/bash
# Find dispatch_event method signature
rg -n "async def dispatch_event|def dispatch_event" backend/app/services/contributor_webhook_service.py -A 10

# Find process_payout function
rg -n "async def process_payout|def process_payout" backend/ -A 20

Repository: SolFoundry/solfoundry

Length of output: 2280


🏁 Script executed:

#!/bin/bash
# Check the full process_payout function to see how PayoutResponse is constructed
rg -n "async def process_payout" backend/app/services/payout_service.py -A 50 | head -70

# Also check if there are any other return paths or exceptions that might not be PayoutResponse
rg -n "return.*PayoutResponse\|return PayoutResponse" backend/app/services/payout_service.py

Repository: SolFoundry/solfoundry

Length of output: 2236


🏁 Script executed:

#!/bin/bash
# Find the _payout_to_response function to understand the conversion
rg -n "_payout_to_response" backend/app/services/payout_service.py -B 5 -A 20

Repository: SolFoundry/solfoundry

Length of output: 5093


Remove unnecessary hasattr checks and fix attribute access pattern.

The code uses defensive hasattr() checks (lines 196-206) even though process_payout() always returns a PayoutResponse instance. The attributes bounty_id, amount, and tx_hash are guaranteed to exist as defined fields.

Additionally, contributor_id does not exist in PayoutResponse and will always be None — this field should be removed or the logic reconsidered.

Instead of the suggested getattr(result, "bounty_id", None) or payout_id pattern (which would incorrectly fall back when bounty_id is legitimately None), directly access the attributes:

-            bounty_id = result.bounty_id if hasattr(result, "bounty_id") else payout_id
-            contributor_id = result.contributor_id if hasattr(result, "contributor_id") else None
+            bounty_id = result.bounty_id or payout_id  # or use bounty_id directly if None is valid
+            contributor_id = None  # PayoutResponse has no contributor_id field
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/payouts.py` around lines 187 - 216, The execute_payout
handler is using unnecessary hasattr checks for attributes that always exist on
PayoutResponse and is referencing a non‑existent contributor_id; replace the
hasattr usages by directly reading fields from the PayoutResponse returned by
process_payout (e.g. use result.bounty_id, str(result.amount), result.tx_hash)
and remove the contributor_id logic entirely; if you need to fall back to
payout_id when bounty_id is explicitly None, do an explicit None check
(bounty_id = result.bounty_id if result.bounty_id is not None else payout_id)
rather than getattr(..., None) or truthy "or" fallbacks, and call
ContributorWebhookService.dispatch_event with user_id omitted or None.

Comment on lines +54 to +57
def __init__(self, app, redis_url: str = os.getenv("REDIS_URL", "redis://localhost:6379")):
super().__init__(app)
self.redis = redis.from_url(redis_url, decode_responses=True)
self._lua_script = None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect Redis client initialization and timeout-related options in middleware.
rg -n -C3 'redis\.from_url|socket_timeout|socket_connect_timeout|health_check_interval' backend/app/middleware/rate_limit.py

# Find other Redis client constructions for consistency comparison.
rg -n --type=py 'from_url\(|Redis\(' backend/app

Repository: SolFoundry/solfoundry

Length of output: 902


🏁 Script executed:

# Get full context of the rate_limit.py middleware to see line 77 and error handling
head -100 backend/app/middleware/rate_limit.py | cat -n

Repository: SolFoundry/solfoundry

Length of output: 4410


🏁 Script executed:

# Check if there's more content in the file and see the fail-open mechanism mentioned at line 77
wc -l backend/app/middleware/rate_limit.py
sed -n '70,85p' backend/app/middleware/rate_limit.py | cat -n

Repository: SolFoundry/solfoundry

Length of output: 978


🏁 Script executed:

# Search for timeout handling throughout the middleware
rg -n "timeout|except|except.*redis|try" backend/app/middleware/rate_limit.py | head -30

Repository: SolFoundry/solfoundry

Length of output: 157


🏁 Script executed:

# Check if there are any socket_timeout or connection pool timeout configurations elsewhere in codebase
rg -n "socket_timeout|connect_timeout|timeout|ConnectionPool|pool" backend/app --type=py | grep -i redis | head -20

Repository: SolFoundry/solfoundry

Length of output: 194


🏁 Script executed:

# Check the redis library version and configuration patterns
cat backend/requirements.txt 2>/dev/null || cat setup.py 2>/dev/null | grep -i redis

Repository: SolFoundry/solfoundry

Length of output: 785


🏁 Script executed:

# Look for any timeout configuration in environment or config files
find backend -name "*.py" -o -name "*.env*" -o -name "*.yaml" -o -name "*.yml" | xargs grep -l "REDIS\|timeout" 2>/dev/null | head -10

Repository: SolFoundry/solfoundry

Length of output: 344


🏁 Script executed:

# Check core config for timeout settings
cat -n backend/app/core/config.py | head -50

Repository: SolFoundry/solfoundry

Length of output: 1763


🏁 Script executed:

# Check redis.py initialization
cat -n backend/app/core/redis.py

Repository: SolFoundry/solfoundry

Length of output: 1319


🏁 Script executed:

# Check .env.example for REDIS configuration
cat -n backend/.env.example | grep -i redis

Repository: SolFoundry/solfoundry

Length of output: 166


Redis client initialization lacks explicit socket timeouts, causing request delays during network degradation.

Line 56 initializes the Redis client with redis.from_url(redis_url, decode_responses=True) without explicit timeout parameters. During degraded Redis/network conditions, async operations can hang indefinitely on socket operations until the connection is forcibly closed by the OS or network, even though the exception handler at lines 76-78 eventually provides a fail-open fallback. This causes unnecessary request latency and potential resource exhaustion in production.

The redis>=5.0 library supports timeout configuration via socket_timeout and socket_connect_timeout parameters in from_url(). This same pattern of missing timeouts is replicated across the codebase in: backend/app/services/websocket_manager.py:78, backend/app/core/redis.py:20, backend/app/api/health.py:52, and backend/app/api/admin.py:900.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/middleware/rate_limit.py` around lines 54 - 57, The Redis client
in RateLimitMiddleware.__init__ initializes self.redis via
redis.from_url(redis_url, decode_responses=True) without socket timeouts; update
that call to pass explicit socket_connect_timeout and socket_timeout (e.g.,
short connect timeout and reasonable read timeout) so socket operations fail
fast during network degradation, then retain the existing fail-open error
handling around _lua_script and request processing; apply the same change to
other Redis initializations that use redis.from_url in websocket_manager
(WebsocketManager), core/redis (module-level client factory), api/health (health
check client), and api/admin (admin Redis client) to ensure consistent timeout
behavior across the codebase.

Comment on lines +82 to +83
if request.url.path == "/api/health":
return await call_next(request)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify registered health routes and prefixes.
rg -n -C3 '@router\.get\("/health"|@app\.(get|api_route)\("/health"|include_router\(|prefix=' backend/app --type=py

# Check if middleware skip logic covers all health endpoints.
rg -n -C2 'request\.url\.path == "/api/health"|/health' backend/app/middleware/rate_limit.py backend/app/main.py backend/app/api/health.py

Repository: SolFoundry/solfoundry

Length of output: 8574


🏁 Script executed:

# Check the health_router definition in health.py
head -100 backend/app/api/health.py | grep -A 5 "router ="

# Also check for any other health-related endpoints or configurations
rg -n "health|Health" backend/app/api/health.py | head -20

# Verify the actual routes and their full paths
rg -n "@router\.|@app\." backend/app/api/health.py

Repository: SolFoundry/solfoundry

Length of output: 1587


🏁 Script executed:

# Search for any other health endpoint definitions
rg -n "@router\.get.*health|@app\.get.*health|@router\.post.*health" backend/app --type=py

# Check for any route prefixing or URL rewriting that might affect health endpoints
rg -n "url\.path|request\.path|/api/health|/health" backend/app/middleware/ backend/app/main.py --type=py | head -30

# Verify if there's a root /health endpoint
rg -n "@app\.get\(\"/health" backend/app --type=py

Repository: SolFoundry/solfoundry

Length of output: 1500


🏁 Script executed:

# Check the rate_limiter.py EXEMPT_PATHS definition and context
sed -n '60,75p' backend/app/middleware/rate_limiter.py

# Check app middleware registration to see which is actually used
rg -n "add_middleware|RateLimit" backend/app/main.py --type=py -A 2 -B 2

# Verify if there's a root-level /health endpoint
rg -n "^@app\.get.*health|^app\.add_route.*health" backend/app/main.py --type=py

Repository: SolFoundry/solfoundry

Length of output: 1395


🏁 Script executed:

# Get the full context of rate_limiter.py to understand the startswith check
sed -n '295,310p' backend/app/middleware/rate_limiter.py

# Verify the actual registered health endpoint path by checking main.py again
sed -n '200,210p' backend/app/main.py

# Check if there's any other route or middleware serving /health at root
rg -n "prefix=|/health" backend/app/api/health.py | head -5

Repository: SolFoundry/solfoundry

Length of output: 1309


🏁 Script executed:

# Check if there's a root-level /health endpoint or app.get decorator
rg -n "^@app\.get|^@app\.post|^@app\.api_route.*health" backend/app --type=py

# Check for any middleware or routing that might serve /health at root
rg -n "app\.get\(\"/health|app\.post\(\"/health" backend/app --type=py

# Verify the complete path: health_router prefix + endpoint path
grep -n "health_router" backend/app/main.py

Repository: SolFoundry/solfoundry

Length of output: 191


🏁 Script executed:

# Verify the logs mentioned in the review to understand what paths are being used
ls -la backend/logs/ 2>/dev/null || echo "logs directory not found"

# Look at the log file mentioned in the review
head -20 backend/logs/application.log.2026-03-21 2>/dev/null || echo "log file not found"

# Search for any alternative health endpoint definitions
rg -n "get\(.*health|post\(.*health|health.*route" backend/app --type=py -i

Repository: SolFoundry/solfoundry

Length of output: 3713


🏁 Script executed:

# Check the exact middleware bypass logic in rate_limiter.py - look at the full dispatch method
sed -n '275,320p' backend/app/middleware/rate_limiter.py

# Search for any app-level health endpoint or root /health serving
rg -n "def.*health|class.*health" backend/app --type=py -i | grep -v "Comprehensive\|health check\|health\|Health"

# Check if there's a livenessProbe or readinessProbe endpoint (common Kubernetes patterns)
rg -n "liveness|readiness|alive|ready" backend/app --type=py -i

Repository: SolFoundry/solfoundry

Length of output: 7438


Health-check rate-limit bypass checks both fail for the registered /api/v2/health endpoint.

The middleware at line 82 bypasses only the non-existent /api/health path, but the actual health endpoint is registered at /api/v2 prefix (main.py:205: app.include_router(health_router, prefix="/api/v2")), making the endpoint accessible at /api/v2/health. Additionally, the alternate RateLimiterMiddleware (rate_limiter.py:298) exempts paths starting with /health, which also fails to match /api/v2/health. Logs confirm /health requests traverse rate-limit middleware and encounter Redis errors, confirming health endpoints are not properly isolated from rate-limiting.

Fix: Update both middleware bypass checks to include the actual endpoint path (/api/v2/health), or use wildcard matching (**/health) to cover all future health endpoint variations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/middleware/rate_limit.py` around lines 82 - 83, The health-check
bypass currently only matches "/api/health" and misses the registered
"/api/v2/health"; update the check in backend/app/middleware/rate_limit.py (the
branch that returns await call_next(request)) to also allow "/api/v2/health" —
better: replace the exact-path check with a more robust match (e.g.,
request.url.path.endswith("/health") or a contains "/health" rule) so any
prefixed health route passes through; also update the alternate
RateLimiterMiddleware exemption in backend/app/middleware/rate_limiter.py (the
logic around paths starting with "/health") to use the same robust matching
(endswith or wildcard) so both middleware implementations consistently bypass
all health endpoints.

Comment on lines +35 to +43
# CSP directives
CSP_DEFAULT_SRC: str = os.getenv("CSP_DEFAULT_SRC", "'self'")
CSP_SCRIPT_SRC: str = os.getenv("CSP_SCRIPT_SRC", "'self'")
CSP_STYLE_SRC: str = os.getenv("CSP_STYLE_SRC", "'self' 'unsafe-inline'")
CSP_IMG_SRC: str = os.getenv("CSP_IMG_SRC", "'self' data: https:")
CSP_SCRIPT_SRC: str = os.getenv("CSP_SCRIPT_SRC", "'self' 'unsafe-inline' 'unsafe-eval'")
CSP_STYLE_SRC: str = os.getenv("CSP_STYLE_SRC", "'self' 'unsafe-inline' https://fonts.googleapis.com")
CSP_IMG_SRC: str = os.getenv("CSP_IMG_SRC", "'self' data: https: https://solfoundry.org")
CSP_CONNECT_SRC: str = os.getenv(
"CSP_CONNECT_SRC", "'self' https://api.mainnet-beta.solana.com"
)
CSP_FONT_SRC: str = os.getenv("CSP_FONT_SRC", "'self'")
CSP_FONT_SRC: str = os.getenv("CSP_FONT_SRC", "'self' https://fonts.gstatic.com")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

❓ Verification inconclusive

FastAPI custom Swagger UI with CSP nonce 2025


To run FastAPI Swagger UI under a strict CSP (with nonces) you generally need to (1) disable FastAPI’s built-in /docs, (2) serve a custom /docs HTML where you add nonce="..." to the inline <script> / <style> tags, and (3) set a per-request CSP header that includes those nonces. FastAPI supports custom Swagger UI routes via get_swagger_ui_html. [1]

Key gotcha (Swagger UI + CSP)

Even if you nonce all inline scripts, Swagger UI has historically required unsafe-eval in some configurations/versions (due to runtime code generation), so a CSP that forbids eval may still result in a blank/broken UI unless you allow unsafe-eval (at least on the docs route) or use a Swagger UI build/version that avoids it. [2], [3]

FastAPI example: custom /docs with CSP nonces

import secrets
from fastapi import FastAPI
from fastapi.openapi.docs import get_swagger_ui_html
from starlette.responses import HTMLResponse

app = FastAPI(docs_url=None, redoc_url=None)

`@app.get`("/docs", include_in_schema=False)
def custom_swagger_ui():
    nonce = secrets.token_urlsafe(16)

    html = get_swagger_ui_html(
        openapi_url=app.openapi_url,
        title="API docs",
        # swagger_ui_parameters={...},  # optional
    ).body.decode("utf-8")

    # Add nonce to inline tags (FastAPI’s docs HTML includes inline JS/CSS)
    html = html.replace("<script>", f'<script nonce="{nonce}">')
    html = html.replace("<style>", f'<style nonce="{nonce}">')

    csp = (
        "default-src 'self'; "
        f"script-src 'self' 'nonce-{nonce}' 'unsafe-eval'; "
        f"style-src 'self' 'nonce-{nonce}'; "
        "img-src 'self' data:; "
        "font-src 'self' data:; "
        "connect-src 'self'; "
        "base-uri 'none'; "
        "frame-ancestors 'none'"
    )

    return HTMLResponse(html, headers={"Content-Security-Policy": csp})

Practical advice: keep this relaxed CSP only on /docs, and use a stricter CSP elsewhere (Swagger UI’s needs are often incompatible with a fully locked-down global CSP). [2], [3]

Sources: FastAPI docs customization entry point [1]; Swagger UI CSP/eval problem reports [2], [3].

[1] (fastapi.tiangolo.com)
[2] (github.com)
[3] (community.smartbear.com)

Citations:


Script executed:

# Find and read the security middleware file
find . -type f -name "security.py" -path "*/middleware/*" | head -5

Repository: SolFoundry/solfoundry


Repository: SolFoundry/solfoundry
Exit code: 0

stdout:

./backend/app/middleware/security.py

Script executed:

# Once found, read the security.py file with context around lines 35-43 and check the full middleware
cd backend && find . -type f -name "security.py" -path "*/middleware/*" -exec cat -n {} \; | head -150

Repository: SolFoundry/solfoundry


Repository: SolFoundry/solfoundry
Exit code: 0

stdout:

     1	"""HTTP security headers middleware for production hardening.
     2	
     3	Implements comprehensive security headers following OWASP recommendations:
     4	- Strict-Transport-Security (HSTS) with preload
     5	- Content-Security-Policy (CSP) restricting resource origins
     6	- X-Frame-Options preventing clickjacking
     7	- X-Content-Type-Options preventing MIME sniffing
     8	- Referrer-Policy limiting referrer information leakage
     9	- Permissions-Policy restricting browser feature access
    10	- Cache-Control headers for sensitive endpoints
    11	- Request body size enforcement to prevent resource exhaustion
    12	"""
    13	
    14	import logging
    15	import os
    16	from typing import Callable
    17	
    18	from fastapi import Request, Response
    19	from starlette.middleware.base import BaseHTTPMiddleware
    20	from starlette.responses import JSONResponse
    21	
    22	logger = logging.getLogger(__name__)
    23	
    24	# Maximum request body size in bytes (default 1 MB)
    25	MAX_REQUEST_BODY_SIZE: int = int(
    26	    os.getenv("MAX_REQUEST_BODY_SIZE", str(1 * 1024 * 1024))
    27	)
    28	
    29	# Whether to enforce HTTPS (disable in local dev)
    30	ENFORCE_HTTPS: bool = os.getenv("ENFORCE_HTTPS", "true").lower() == "true"
    31	
    32	# HSTS max-age in seconds (default: 1 year)
    33	HSTS_MAX_AGE: int = int(os.getenv("HSTS_MAX_AGE", "31536000"))
    34	
    35	# CSP directives
    36	CSP_DEFAULT_SRC: str = os.getenv("CSP_DEFAULT_SRC", "'self'")
    37	CSP_SCRIPT_SRC: str = os.getenv("CSP_SCRIPT_SRC", "'self' 'unsafe-inline' 'unsafe-eval'")
    38	CSP_STYLE_SRC: str = os.getenv("CSP_STYLE_SRC", "'self' 'unsafe-inline' https://fonts.googleapis.com")
    39	CSP_IMG_SRC: str = os.getenv("CSP_IMG_SRC", "'self' data: https: https://solfoundry.org")
    40	CSP_CONNECT_SRC: str = os.getenv(
    41	    "CSP_CONNECT_SRC", "'self' https://api.mainnet-beta.solana.com"
    42	)
    43	CSP_FONT_SRC: str = os.getenv("CSP_FONT_SRC", "'self' https://fonts.gstatic.com")
    44	CSP_FRAME_ANCESTORS: str = os.getenv("CSP_FRAME_ANCESTORS", "'none'")
    45	
    46	# Paths considered sensitive (no caching)
    47	SENSITIVE_PATH_PREFIXES: tuple[str, ...] = (
    48	    "/auth/",
    49	    "/api/payouts",
    50	    "/api/treasury",
    51	)
    52	
    53	def _build_csp_header() -> str:
    54	    directives = [
    55	        f"default-src {CSP_DEFAULT_SRC}",
    56	        f"script-src {CSP_SCRIPT_SRC}",
    57	        f"style-src {CSP_STYLE_SRC}",
    58	        f"img-src {CSP_IMG_SRC}",
    59	        f"connect-src {CSP_CONNECT_SRC}",
    60	        f"font-src {CSP_FONT_SRC}",
    61	        f"frame-ancestors {CSP_FRAME_ANCESTORS}",
    62	        "base-uri 'self'",
    63	        "form-action 'self'",
    64	        "object-src 'none'",
    65	        "upgrade-insecure-requests",
    66	    ]
    67	    return "; ".join(directives)
    68	
    69	def _build_permissions_policy() -> str:
    70	    policies = [
    71	        "camera=()", "microphone=()", "geolocation=()", "payment=()",
    72	        "usb=()", "magnetometer=()", "gyroscope=()", "accelerometer=()",
    73	    ]
    74	    return ", ".join(policies)
    75	
    76	class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    77	    """OWASP-recommended security headers and request body size enforcement."""
    78	
    79	    def __init__(self, app: Callable) -> None:
    80	        super().__init__(app)
    81	        self.csp_header = _build_csp_header()
    82	        self.permissions_policy = _build_permissions_policy()
    83	
    84	    async def dispatch(self, request: Request, call_next: Callable) -> Response:
    85	        # Enforce request body size limit
    86	        content_length = request.headers.get("content-length")
    87	        if content_length and int(content_length) > MAX_REQUEST_BODY_SIZE:
    88	            return JSONResponse(
    89	                status_code=413,
    90	                content={"detail": "Request body too large", "code": "PAYLOAD_TOO_LARGE"}
    91	            )
    92	
    93	        response = await call_next(request)
    94	
    95	        # Apply Headers
    96	        if ENFORCE_HTTPS:
    97	            response.headers["Strict-Transport-Security"] = f"max-age={HSTS_MAX_AGE}; includeSubDomains; preload"
    98	        
    99	        response.headers["Content-Security-Policy"] = self.csp_header
   100	        response.headers["X-Frame-Options"] = "DENY"
   101	        response.headers["X-Content-Type-Options"] = "nosniff"
   102	        response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
   103	        response.headers["Permissions-Policy"] = self.permissions_policy
   104	
   105	        if any(request.url.path.startswith(p) for p in SENSITIVE_PATH_PREFIXES):
   106	            response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
   107	            response.headers["Pragma"] = "no-cache"
   108	
   109	        if "Server" in response.headers:
   110	            del response.headers["Server"]
   111	
   112	        return response
   113	
   114	# Alias for backward compatibility with existing tests
   115	SecurityMiddleware = SecurityHeadersMiddleware

Script executed:

# Check if /docs is disabled or customized in main app
find . -type f \( -name "main.py" -o -name "app.py" -o -name "__init__.py" \) -path "*/backend/*" -exec grep -l "docs_url\|redoc_url\|swagger_ui_html" {} \;

Repository: SolFoundry/solfoundry


Repository: SolFoundry/solfoundry
Exit code: 0


CSP relaxation applies globally and should be route-specific to minimize XSS exposure.

The CSP header at lines 37–39 permits 'unsafe-inline' and 'unsafe-eval' on scripts and is applied globally to every response (line 99 in the middleware). This weakens XSS protections across the entire API, not just for the /docs endpoint.

While 'unsafe-eval' is historically necessary for Swagger UI to function, the current global application is overly permissive. FastAPI's /docs should use a custom route with nonce-based CSP (as documented in FastAPI's configuration guide), keeping the global CSP strict and restrictive for all other endpoints.

Recommended fix: Disable the default /docs and /redoc routes, implement a custom /docs handler that injects nonces into inline scripts/styles, and apply a relaxed CSP only to that route. Keep the global middleware CSP strict (removing 'unsafe-inline' and 'unsafe-eval').

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/middleware/security.py` around lines 35 - 43, The CSP is
currently too permissive globally via CSP_SCRIPT_SRC and CSP_STYLE_SRC (they
include 'unsafe-inline' and 'unsafe-eval') and is applied in the global
middleware in security.py; change the global CSP to remove 'unsafe-inline' and
'unsafe-eval' (tighten
CSP_DEFAULT_SRC/CSP_SCRIPT_SRC/CSP_STYLE_SRC/CSP_IMG_SRC/CSP_CONNECT_SRC/CSP_FONT_SRC)
and then disable FastAPI's default /docs and /redoc routes, creating a custom
/docs handler that injects per-request nonces into inline scripts/styles and
applies a relaxed CSP header only for that route; update the middleware that
sets the response header to use the strict env-based values by default and add
route-level logic to set the relaxed CSP when serving the custom docs endpoint.

Comment on lines +1 to +2
{"message": "RateLimiter Redis Error: Error Multiple exceptions: [Errno 61] Connect call failed ('::1', 6379, 0, 0), [Errno 61] Connect call failed ('127.0.0.1', 6379) connecting to localhost:6379. - Failsafe: ALLOW", "taskName": "starlette.middleware.base.BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.coro"}
{"message": "RateLimiter/Blocklist Redis Error: Error 1 connecting to localhost:6379. Operation not permitted. - Failsafe: ALLOW", "taskName": "starlette.middleware.base.BaseHTTPMiddleware.__call__.<locals>.call_next.<locals>.coro"}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Runtime error logs should not be committed to source control.

Lines 1-2 contain environment-specific operational failures and fail-open security behavior details. Keeping these artifacts in the repo increases leakage risk and creates non-deterministic diff noise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/logs/error.log.2026-03-21` around lines 1 - 2, This commit
accidentally includes runtime error logs (entries mentioning "RateLimiter",
"Blocklist", "Failsafe: ALLOW", and the Starlette taskName
"starlette.middleware.base.BaseHTTPMiddleware.__call__...coro"); remove that log
file from the repo, add the log filename pattern (e.g., error.log.* or the
specific runtime log name) to .gitignore, and revert the file from the commit
history so operational logs are not tracked; additionally, ensure your logging
config writes to a non-source-control sink (rotating file, syslog, or cloud
logs) and add a pre-commit/CI check to prevent committed runtime logs in the
future.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/tests/e2e/conftest.py (1)

88-96: ⚠️ Potential issue | 🟠 Major

Shared import-time event loop is still a suite-wide reliability hazard.

Lines 88-96 reintroduce a module-global loop via _get_test_loop(), and Lines 124-128 drive engine.begin() through run_until_complete() while conftest.py is importing. That makes test collection depend on async database I/O before fixture setup and leaves initialization running on a different loop from the one later used by pytest-asyncio/anyio in async_client. The failure mode is suite-wide: if that loop is stale, blocked, or incompatible with later async fixtures, collection/import breaks before the tests even start. This is the same unresolved loop-pollution/import-order issue already raised on earlier revisions.

Also applies to: 102-128

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/e2e/conftest.py` around lines 88 - 96, The module-global
_test_loop and the import-time call driving engine.begin() cause loop pollution
and run async DB I/O during import; remove the _test_loop/_get_test_loop usage
and stop calling engine.begin() at import time in conftest.py, and instead
perform database initialization inside an async fixture (e.g., a
pytest-asyncio/anyio session-scoped fixture used by async_client) so
engine.begin() is awaited on the test-run event loop; update any places
referencing _get_test_loop to use the fixture-provided loop or run
engine.begin() inside that fixture so all async work uses the same
pytest-managed loop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/tests/e2e/conftest.py`:
- Around line 30-33: The tests currently use a single temp-file SQLite DB
(TEST_DB_PATH, DATABASE_URL) and call Base.metadata.create_all() while
clear_stores() only resets in-memory stores, which causes persistent state and
fails to validate PostgreSQL-specific behaviors; update conftest.py to spin up
an ephemeral PostgreSQL 15+ instance for E2E (use a test container or in-memory
PG harness), set DATABASE_URL to that instance instead of TEST_DB_PATH, run
Alembic migrations (not Base.metadata.create_all()) during async initialization,
and extend the teardown/clear_stores() fixture to truncate all DB tables and
destroy the ephemeral PG instance to ensure full isolation and correct full-text
search semantics validation.

---

Duplicate comments:
In `@backend/tests/e2e/conftest.py`:
- Around line 88-96: The module-global _test_loop and the import-time call
driving engine.begin() cause loop pollution and run async DB I/O during import;
remove the _test_loop/_get_test_loop usage and stop calling engine.begin() at
import time in conftest.py, and instead perform database initialization inside
an async fixture (e.g., a pytest-asyncio/anyio session-scoped fixture used by
async_client) so engine.begin() is awaited on the test-run event loop; update
any places referencing _get_test_loop to use the fixture-provided loop or run
engine.begin() inside that fixture so all async work uses the same
pytest-managed loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fa065a82-b930-431d-9dcc-ebbf143c3369

📥 Commits

Reviewing files that changed from the base of the PR and between 60f63e3 and eafd2a8.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • backend/app/services/payout_service.py
  • backend/app/services/treasury_service.py
  • backend/tests/e2e/conftest.py

Comment on lines +30 to +33
# Ensure the test database path is absolute and platform-agnostic
TEST_DB_PATH = os.path.join(tempfile.gettempdir(), "test_e2e.db")
DATABASE_URL = f"sqlite+aiosqlite:///{TEST_DB_PATH}"
os.environ["DATABASE_URL"] = DATABASE_URL
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's view the conftest.py file to see the actual code
cat -n backend/tests/e2e/conftest.py | head -200

Repository: SolFoundry/solfoundry

Length of output: 9332


🏁 Script executed:

# Check for migration/alembic setup
find backend -name "alembic.ini" -o -name "migrations" -o -name "alembic" -type d 2>/dev/null | head -20

Repository: SolFoundry/solfoundry

Length of output: 146


🏁 Script executed:

# Search for database initialization patterns in the codebase
rg -A 5 "Base.metadata.create_all|alembic|migration" backend --type py | head -60

Repository: SolFoundry/solfoundry

Length of output: 3793


🏁 Script executed:

# Check the actual Mission_82_Specs.md to see test environment requirements
cat Mission_82_Specs.md 2>/dev/null | head -100

Repository: SolFoundry/solfoundry

Length of output: 2015


🏁 Script executed:

# Check if there's any database cleanup in conftest or test setup
rg "truncate|delete from|drop table|rm.*\.db" backend/tests/e2e --type py

Repository: SolFoundry/solfoundry

Length of output: 47


Persistent SQLite test DB prevents proper E2E validation of Mission #82 PostgreSQL requirements and creates cross-test contamination.

Lines 30–33 pin all E2E tests to the same temp-file SQLite database, lines 124–128 use Base.metadata.create_all() instead of running Alembic migrations, and the clear_stores() fixture at lines 177–180 clears only in-memory stores—it never truncates database tables or removes the temp file. This means rows and schema persist across test runs, preventing test isolation. More critically, Mission #82's acceptance criteria explicitly require PostgreSQL 15+ with full-text search semantics (weighted TSVECTOR, ?| vs ?& operators for skill filtering, specific ranking behavior), but this harness exercises only SQLite. E2E tests therefore cannot validate PostgreSQL-specific search behavior, weight ordering, or the migration history—only whether the code compiles against SQLAlchemy's generic schema layer. That is a material acceptance-path gap.

Also applies to: 88–96 (shared event loop), 124–128 (async initialization), 177–180 (cleanup scope).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/e2e/conftest.py` around lines 30 - 33, The tests currently use
a single temp-file SQLite DB (TEST_DB_PATH, DATABASE_URL) and call
Base.metadata.create_all() while clear_stores() only resets in-memory stores,
which causes persistent state and fails to validate PostgreSQL-specific
behaviors; update conftest.py to spin up an ephemeral PostgreSQL 15+ instance
for E2E (use a test container or in-memory PG harness), set DATABASE_URL to that
instance instead of TEST_DB_PATH, run Alembic migrations (not
Base.metadata.create_all()) during async initialization, and extend the
teardown/clear_stores() fixture to truncate all DB tables and destroy the
ephemeral PG instance to ensure full isolation and correct full-text search
semantics validation.

@denaev-dev denaev-dev force-pushed the bounty-health-check-v2 branch from d410c07 to 851d3af Compare March 23, 2026 12:16
@denaev-dev denaev-dev force-pushed the bounty-health-check-v2 branch from 851d3af to 560b781 Compare March 23, 2026 12:16
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/app/services/pg_store.py (2)

209-229: ⚠️ Potential issue | 🟠 Major

load_payouts does not load updated_at, failure_reason, retry_count, admin_approved_by fields.

Lines 216-228 construct PayoutRecord but omit these fields. Combined with persist_payout not saving them, this creates a complete data loss loop for payout lifecycle metadata.

Additionally, line 225 PayoutStatus(row.status.lower()) will raise ValueError for unexpected status values, crashing the entire load operation.

🔧 Proposed fix for missing fields
             out[str(row.id)] = PayoutRecord(
                 id=str(row.id),
                 recipient=row.recipient,
                 recipient_wallet=row.recipient_wallet,
                 amount=float(row.amount),
                 token=row.token,
                 bounty_id=str(row.bounty_id) if row.bounty_id else None,
                 bounty_title=row.bounty_title,
                 tx_hash=row.tx_hash,
                 status=PayoutStatus(row.status.lower()),
                 solscan_url=row.solscan_url,
                 created_at=row.created_at,
+                updated_at=getattr(row, "updated_at", row.created_at),
+                failure_reason=getattr(row, "failure_reason", None),
+                retry_count=getattr(row, "retry_count", 0),
+                admin_approved_by=getattr(row, "admin_approved_by", None),
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 209 - 229, The load_payouts
function is missing several payout lifecycle fields and uses an unsafe enum
conversion; update the PayoutRecord construction in load_payouts to include
updated_at, failure_reason, retry_count, and admin_approved_by by reading them
from the PayoutTable row (e.g., row.updated_at, row.failure_reason,
row.retry_count, row.admin_approved_by), and ensure persist_payout also persists
those same fields; additionally replace the brittle
PayoutStatus(row.status.lower()) call with a safe parse (normalize the DB value,
attempt to construct the PayoutStatus and wrap it in a try/except or use a
mapping/fallback to a sensible default like PayoutStatus.UNKNOWN or
PayoutStatus.PENDING) so unexpected status values do not raise ValueError and
crash load_payouts.

188-207: ⚠️ Potential issue | 🔴 Critical

persist_payout is missing critical fields that are set during state transitions: updated_at, failure_reason, retry_count, admin_approved_by.

The PayoutRecord model defines all four fields and the service layer actively sets them (especially updated_at on every state change and failure_reason on failures), but PayoutTable lacks the corresponding columns and persist_payout omits them from the upsert. This causes silent data loss when records are persisted and reloaded from the database.

The docstring in payout.py includes a migration template showing these fields were intended for the schema:

admin_approved_by VARCHAR(100),
retry_count INT NOT NULL DEFAULT 0,
failure_reason TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

Add all four fields to the upsert call at lines 192-206:

    await _upsert(
        session,
        PayoutTable,
        record.id,
        recipient=record.recipient,
        recipient_wallet=record.recipient_wallet,
        amount=record.amount,
        token=record.token,
        bounty_id=_to_uuid(record.bounty_id) if record.bounty_id else None,
        bounty_title=record.bounty_title,
        tx_hash=record.tx_hash,
        status=status,
        solscan_url=record.solscan_url,
        created_at=record.created_at,
+       updated_at=getattr(record, "updated_at", record.created_at),
+       failure_reason=getattr(record, "failure_reason", None),
+       retry_count=getattr(record, "retry_count", 0),
+       admin_approved_by=getattr(record, "admin_approved_by", None),
    )

Note: PayoutTable must first be updated to include these columns before this fix is applied.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 188 - 207, persist_payout
omits fields that are set during state transitions—add updated_at,
failure_reason, retry_count, and admin_approved_by to the _upsert call in
persist_payout so these values from the PayoutRecord are persisted;
specifically, pass updated_at=record.updated_at,
failure_reason=record.failure_reason, retry_count=record.retry_count, and
admin_approved_by=record.admin_approved_by into the existing _upsert(...)
invocation in the persist_payout function (ensure PayoutTable schema has
corresponding columns before applying).
backend/tests/e2e/conftest.py (1)

101-157: ⚠️ Potential issue | 🟠 Major

The E2E clients are not exercising the real application stack.

_create_test_app() rebuilds a bare FastAPI instance with routers only; it does not wire the production lifespan or app-level middleware, and it also adds a stub /health route at Lines 155-157 that returns the stale {"status": "ok"} contract. Because both client and async_client bind to this synthetic app, this suite cannot validate Mission #80 behaviors that live in the real request pipeline: actual health telemetry, unified healthy|degraded|unavailable statuses, request ID / process-time headers, and failsafe behavior when dependencies degrade.

As per coding guidelines, backend/logs/application.log.2026-03-21 expects /health request instrumentation with correlated request_id, and backend/logs/error.log.2026-03-21 requires Redis failures to stay failsafe rather than break health availability.

Also applies to: 195-256

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/e2e/conftest.py` around lines 101 - 157, The test builds a
synthetic FastAPI app in _create_test_app (creating test_app, adding routers and
a stub health_check) which bypasses production lifespan, middleware, and real
/health behavior; replace this by instantiating the real application (use the
production app factory or module that wires lifespan/middleware/telemetry)
instead of creating test_app, remove the inline health_check stub, and keep only
dependency_overrides[get_current_user] = _mock_get_current_user (or apply the
override on the real app) so E2E clients exercise the real request pipeline
(request_id headers, real health handler that returns
healthy|degraded|unavailable, failsafe Redis behavior) while preserving
authentication override for tests.
♻️ Duplicate comments (13)
backend/submit_bounty.py (3)

9-22: ⚠️ Potential issue | 🟠 Major

Dead payload includes hardcoded submission metadata and wallet identifier.

pr_body is never consumed, so Lines 9-22 are dead code. It also embeds hardcoded operational metadata (including a wallet address), which is unnecessary in backend source and increases compliance/audit noise if committed long-term.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/submit_bounty.py` around lines 9 - 22, Remove the unused hardcoded
pr_body string in submit_bounty.py: delete the dead variable definition
(pr_body) and any unreachable block that defines it, ensuring no operational
metadata or wallet identifiers remain in backend source; if the text is required
for documentation, move it to a safe external doc (e.g., a markdown changelog)
or a non-production config, and run tests to confirm no references to pr_body
remain.

27-33: ⚠️ Potential issue | 🔴 Critical

Destructive git workflow is unsafe (git add . + push --force) with hardcoded target branch.

Line 27 stages everything indiscriminately, and Line 33 force-pushes to a hardcoded branch unrelated to this PR branch. This combination can leak unintended files and rewrite remote history, making it a high-risk automation path inside repository code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/submit_bounty.py` around lines 27 - 33, The script uses unsafe git
operations: the subprocess.run calls that execute ["git", "add", "."] and
["git", "push", "origin", "bounty-169-final-certified", "--force"] in
submit_bounty.py should be changed to stage only intended files (avoid "git add
."), determine or accept the target branch dynamically instead of hardcoding
"bounty-169-final-certified", and avoid --force; update the code around the
subprocess.run invocations (the git add/commit/push calls) to accept a whitelist
or explicit file list, use the current branch name or a function parameter for
the target branch, and replace --force with a safe push strategy (e.g., normal
push or --force-with-lease) and/or require confirmation before destructive
pushes.

1-38: ⚠️ Potential issue | 🔴 Critical

Critical scope violation: this file is unrelated to PR #797 backend deliverables.

backend/submit_bounty.py is a git automation script for a different submission context (Bounty 169 / PR #410), not part of the FastAPI backend work for Missions #80-82. Keeping this in backend/ introduces non-product automation into production backend scope and breaks API-contract-focused review boundaries.
As per coding guidelines, "backend/**: Python FastAPI backend. Analyze thoroughly: ... API contract consistency with spec."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/submit_bounty.py` around lines 1 - 38, This file
backend/submit_bounty.py is an unrelated git-automation script (contains run(),
logger, and git subprocess calls) and must be removed or relocated out of the
backend FastAPI package: either delete backend/submit_bounty.py from this
branch/PR, or move it into a dedicated tooling/automation repo or a top-level
tools/ directory (preserving run() if you need the script), then amend the
commit(s) to remove the file from PR `#797`, ensure no imports/reference to
submit_bounty.py remain, update any CI or packaging manifests so it isn't
packaged with the backend, and run the test suite/linters to confirm nothing
breaks.
backend/app/services/payout_service.py (7)

155-179: ⚠️ Potential issue | 🟡 Minor

approve_payout does not update the in-memory cache after persisting.

After persisting the updated record at line 172, the _payout_store cache is not refreshed. Subsequent reads from the cache will return stale status, causing inconsistency between cache and database.

🔧 Proposed fix
     record.status = PayoutStatus.APPROVED
     record.updated_at = datetime.now(timezone.utc)
     await pg_store.persist_payout(record)
+    with _store_lock:
+        _payout_store[record.id] = record
     
     return AdminApprovalResponse(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 155 - 179,
approve_payout updates the DB but never refreshes the in-memory cache, causing
stale reads; after await pg_store.persist_payout(record) you should re-acquire
the _store_lock and update _payout_store[payout_id] = record (or replace the
existing entry) so the cache reflects the new status, ensuring you modify the
same record object or a copy while holding the lock to avoid race conditions
with concurrent readers/writers.

261-270: ⚠️ Potential issue | 🟡 Minor

list_buybacks reports incorrect total count.

Line 262 fetches limit + skip + 100 records, but line 267 reports total=len(items) which only reflects the fetched subset. For pagination beyond this window, totals will be incorrect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 261 - 270, The total
returned by list_buybacks is incorrect because it uses total=len(items) (the
sliced subset) instead of the full set; update list_buybacks to compute the
total from the full result (e.g., use len(all_records) or call a proper count
method on pg_store) and pass that value into BuybackListResponse (keep the
existing paging using items/page and mapping via _buyback_to_response).

181-197: ⚠️ Potential issue | 🟡 Minor

reject_payout does not update the in-memory cache after persisting.

Same issue as approve_payout — after persisting the rejection at line 190, the in-memory _payout_store is not updated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 181 - 197, reject_payout
currently persists the updated record with pg_store.persist_payout but never
updates the in-memory cache, mirroring the same bug in approve_payout; after the
persist call in reject_payout, update the module-level cache (e.g.,
_payout_store) with the new record (for example _payout_store[payout_id] =
record or update the existing entry's fields) so the in-memory state matches the
DB before returning the AdminApprovalResponse.

242-259: ⚠️ Potential issue | 🟠 Major

Buyback duplicate detection is limited and lacks DB constraint error handling.

Lines 243-247 only scan the newest 1000 buybacks for duplicate tx_hash. BuybackTable.tx_hash has a UNIQUE constraint, so duplicates outside this window—or concurrent inserts—will raise an unhandled database integrity error (500) instead of the controlled 400.

Wrap pg_store.persist_buyback to catch IntegrityError and convert to HTTPException(400, ...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 242 - 259, The
duplicate-check in create_buyback currently only scans the latest 1000 records
and does not handle DB UNIQUE violations; wrap the call to
pg_store.persist_buyback in a try/except that catches the DB integrity exception
(e.g., IntegrityError from your DB library) and raise
HTTPException(status_code=400, detail="Buyback already exists") when tx_hash
uniqueness is violated (referencing the BuybackTable.tx_hash constraint), then
continue to update _buyback_store[record.id] and return
_buyback_to_response(record) on success; keep the existing pre-check loop but
add this DB-level error handling around persist_buyback to handle duplicates
outside the scanned window or concurrent inserts.

110-127: ⚠️ Potential issue | 🟠 Major

In-memory filtering produces incorrect pagination totals.

Line 110 fetches a bounded slice (limit + skip + 1000), then applies filters in-memory. Line 126 reports total = len(items) which only reflects the filtered subset of fetched records, not the true database count. For datasets exceeding this window:

  • Pagination metadata will be incorrect
  • Records beyond the fetch window are invisible
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 110 - 127, The
pagination is incorrect because the code fetches a bounded slice via
pg_store.load_payouts(limit=limit + skip + 1000) then applies filters in-memory
(variables items, recipient, status, bounty_id, token, start_date, end_date) and
sets total = len(items), which only counts the filtered subset within the fetch
window; fix by moving filtering into the database layer and using a proper count
query: extend or replace pg_store.load_payouts to accept filter parameters
(recipient, status, bounty_id, token, start_date, end_date) and an offset/limit,
then call a new pg_store.count_payouts(filters) to get the true total, and
finally retrieve the paged rows with pg_store.load_payouts(filters, limit, skip)
so total reflects the DB count and page contains correct records.

272-294: ⚠️ Potential issue | 🟠 Major

Aggregate functions truncate at 10,000 records.

get_total_buybacks, get_total_paid_out, _count_confirmed_payouts, and _count_buybacks all call pg_store.load_* with limit=10000. Once the tables grow past this size, aggregates will be understated.

Use database-side aggregation (SUM, COUNT) instead of loading records into memory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 272 - 294, The four
service functions get_total_buybacks, get_total_paid_out,
_count_confirmed_payouts, and _count_buybacks currently call
pg_store.load_buybacks/load_payouts with limit=10000 which truncates results;
replace in-memory aggregation with database-side aggregates by adding and
calling new pg_store methods (e.g., sum_buybacks(),
sum_confirmed_payouts_by_token(token), count_confirmed_payouts(),
count_buybacks()) that run SQL SUM/COUNT queries (with proper WHERE for
PayoutStatus.CONFIRMED and token="FNDRY"/"SOL"), then update get_total_buybacks,
get_total_paid_out, _count_confirmed_payouts, and _count_buybacks to await those
pg_store aggregate methods and return the aggregated numbers.

61-94: ⚠️ Potential issue | 🟠 Major

Duplicate-pay prevention is still racy and limited to 5000 records.

The bounty-level duplicate check at lines 71-75 loads only 5000 payouts from the database. This:

  1. Misses older payouts beyond the limit window
  2. Is not atomic with the subsequent insert, allowing concurrent requests to race past each other
  3. Does not rely on database-level uniqueness constraints

The tx_hash check at lines 65-68 only scans the in-memory cache, not the database, so duplicates in DB but not in cache will be missed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 61 - 94, The
duplicate-prevention in create_payout is racy and incomplete: stop relying on
load_payouts(limit=5000) and the in-memory _payout_store scan and instead
enforce uniqueness at the DB and handle conflicts atomically—add DB unique
constraints/indexes for tx_hash (unique if non-null) and for bounty_id (unique
or unique partial depending on nullable semantics), modify
pg_store.persist_payout to perform the insert and surface
IntegrityError/unique-constraint violations, and in create_payout call
persist_payout directly (or call a new pg_store.insert_payout_if_not_exists) and
catch the DB integrity exception to raise HTTPException(400) for duplicate
tx_hash or bounty_id; keep the in-memory _payout_store updates as caching only,
and remove/replace load_payouts usage in create_payout (references:
create_payout, pg_store.load_payouts, pg_store.persist_payout, PayoutRecord,
PayoutCreate).
backend/app/services/pg_store.py (1)

276-280: 🧹 Nitpick | 🔵 Trivial

save_last_sync always inserts a new row, causing unbounded table growth.

Line 279 unconditionally adds a new SyncStateTable row on every call. Over time, this creates unbounded growth in the sync_state table. Consider using upsert with a fixed key or deleting old rows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/pg_store.py` around lines 276 - 280, The save_last_sync
function currently always inserts a new SyncStateTable row causing unbounded
table growth; change save_last_sync to update or upsert the single canonical
sync row instead of inserting every time: locate save_last_sync and replace the
unconditional session.add(SyncStateTable(last_sync=dt)) with an upsert/update
flow (e.g., query for the existing SyncStateTable row by its unique key or
primary id and update its last_sync, or use the ORM/session.merge or DB-level ON
CONFLICT upsert to set last_sync = :dt), or alternatively delete old rows before
inserting so only one row remains; ensure this uses get_db_session() and commits
as before.
backend/tests/e2e/conftest.py (2)

87-95: ⚠️ Potential issue | 🟠 Major

Import-time loop bootstrap is still fighting pytest-asyncio.

Line 94 globally installs a shared manual event loop, Line 127 uses it to run async schema initialization, and Line 171 does all of that during module import before pytest fixtures are active. async_client at Lines 247-256 then runs requests on pytest-managed loops, so the harness mixes resources created on one loop with tests executing on another. That is the same cross-loop/test-pollution failure mode already flagged earlier, and the shared loop is still never closed or cleared.

Also applies to: 123-127, 171-171, 247-256

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/e2e/conftest.py` around lines 87 - 95, The code installs and
reuses a global event loop at import time via _test_loop/_get_test_loop and
calls asyncio.set_event_loop and runs async schema initialization before pytest
fixtures, causing cross-loop pollution with pytest-asyncio and never closing the
loop; fix by removing import-time loop creation and asyncio.set_event_loop
usage, move any async schema initialization into an async pytest fixture (or use
pytest-asyncio's built-in event_loop/async_client fixtures) so initialization
runs on pytest-managed loops, ensure the created loop (if any) is closed and
cleared in teardown, and update references from _get_test_loop/_test_loop to
rely on the pytest event loop instead of a global loop.

30-33: ⚠️ Potential issue | 🟠 Major

Shared SQLite + create_all() makes Mission #82 E2E results non-representative.

Line 33 overwrites DATABASE_URL unconditionally to one temp-file SQLite database, Lines 124-125 build schema with Base.metadata.create_all(), and Lines 180-187 never truncate tables or remove that file. That reuses stale rows/schema across runs, create_all() will not evolve an existing test_e2e.db, and this harness cannot be pointed at PostgreSQL to exercise the search behavior this PR adds (ts_rank_cd, ?| / ?&, trigger-driven search_vector rebuild, project_name migration/backfill). A green E2E run here does not validate the shipped Mission #82 path.

As per coding guidelines, backend/migrations/V2__add_project_name_and_rebuild_search_vector.sql requires PostgreSQL trigger/backfill logic and weighted to_tsvector tokens for title, project_name, description, and skills.

Also applies to: 123-127, 173-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/e2e/conftest.py` around lines 30 - 33, The test harness
currently forces a single temp-file SQLite DB by unconditionally setting
TEST_DB_PATH and os.environ["DATABASE_URL"] and calling
Base.metadata.create_all(), which reuses stale schema/data and prevents running
tests against PostgreSQL (breaking Mission `#82` validation); change conftest.py
so DATABASE_URL is only set to the tempfile path when no DATABASE_URL exists in
env (respect existing env to allow PostgreSQL), avoid unconditionally calling
Base.metadata.create_all() for production migrations (use a per-test ephemeral
DB or transactional fixtures and ensure tests truncate tables or remove the temp
file between runs), and add teardown logic to delete TEST_DB_PATH or drop all
tables after tests complete; refer to TEST_DB_PATH, DATABASE_URL, and
Base.metadata.create_all() to locate where to implement these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/services/payout_service.py`:
- Around line 186-190: The PayoutRecord fields failure_reason and updated_at are
only set in memory by reject_payout and process_payout but never persisted
because persist_payout (and the PayoutTable schema) don't include those columns;
update the DB schema/migration to add failure_reason (text) and updated_at
(timestamp with timezone) to PayoutTable, update persist_payout to upsert these
two fields, and ensure load_payouts maps those columns back into PayoutRecord so
rejected/failed payouts retain their failure_reason and timestamps; touch the
functions persist_payout, PayoutTable definition, and load_payouts to implement
this change.

In `@backend/submit_bounty.py`:
- Around line 27-33: The three sequential subprocess.run operations (git add,
git commit, git push) can leave partial repo state if a later step fails; wrap
them in a try/except and either perform the operations on a temporary branch or
implement a rollback path: create a branch first (e.g.,
subprocess.run(["git","checkout","-b","bounty-169-final-certified"],...)), run
git add/commit/push, and on any exception run recovery commands (e.g.,
subprocess.run(["git","reset","--soft","HEAD~1"],...) to undo the commit and
subprocess.run(["git","restore","--staged","."]) to clear staging, optionally
delete the temp branch with subprocess.run(["git","checkout","-"],...) and
subprocess.run(["git","branch","-D","bounty-169-final-certified"],...), log the
error and re-raise; ensure all subprocess.run calls still use check=True and
capture output for diagnostics.

In `@backend/tests/e2e/conftest.py`:
- Around line 44-45: The clear_stores() fixture currently resets service stores
but misses resetting the WebSocket singleton; modify clear_stores() to call and
await manager.shutdown() (the imported singleton `manager` from
websocket_manager) so that
_connections/_subscriptions/_rate_buckets/_event_buffer are cleared before each
test, ensuring route-level E2E tests use a fresh WebSocketManager instance; no
changes to the separate websocket_manager fixture are needed.

---

Outside diff comments:
In `@backend/app/services/pg_store.py`:
- Around line 209-229: The load_payouts function is missing several payout
lifecycle fields and uses an unsafe enum conversion; update the PayoutRecord
construction in load_payouts to include updated_at, failure_reason, retry_count,
and admin_approved_by by reading them from the PayoutTable row (e.g.,
row.updated_at, row.failure_reason, row.retry_count, row.admin_approved_by), and
ensure persist_payout also persists those same fields; additionally replace the
brittle PayoutStatus(row.status.lower()) call with a safe parse (normalize the
DB value, attempt to construct the PayoutStatus and wrap it in a try/except or
use a mapping/fallback to a sensible default like PayoutStatus.UNKNOWN or
PayoutStatus.PENDING) so unexpected status values do not raise ValueError and
crash load_payouts.
- Around line 188-207: persist_payout omits fields that are set during state
transitions—add updated_at, failure_reason, retry_count, and admin_approved_by
to the _upsert call in persist_payout so these values from the PayoutRecord are
persisted; specifically, pass updated_at=record.updated_at,
failure_reason=record.failure_reason, retry_count=record.retry_count, and
admin_approved_by=record.admin_approved_by into the existing _upsert(...)
invocation in the persist_payout function (ensure PayoutTable schema has
corresponding columns before applying).

In `@backend/tests/e2e/conftest.py`:
- Around line 101-157: The test builds a synthetic FastAPI app in
_create_test_app (creating test_app, adding routers and a stub health_check)
which bypasses production lifespan, middleware, and real /health behavior;
replace this by instantiating the real application (use the production app
factory or module that wires lifespan/middleware/telemetry) instead of creating
test_app, remove the inline health_check stub, and keep only
dependency_overrides[get_current_user] = _mock_get_current_user (or apply the
override on the real app) so E2E clients exercise the real request pipeline
(request_id headers, real health handler that returns
healthy|degraded|unavailable, failsafe Redis behavior) while preserving
authentication override for tests.

---

Duplicate comments:
In `@backend/app/services/payout_service.py`:
- Around line 155-179: approve_payout updates the DB but never refreshes the
in-memory cache, causing stale reads; after await
pg_store.persist_payout(record) you should re-acquire the _store_lock and update
_payout_store[payout_id] = record (or replace the existing entry) so the cache
reflects the new status, ensuring you modify the same record object or a copy
while holding the lock to avoid race conditions with concurrent readers/writers.
- Around line 261-270: The total returned by list_buybacks is incorrect because
it uses total=len(items) (the sliced subset) instead of the full set; update
list_buybacks to compute the total from the full result (e.g., use
len(all_records) or call a proper count method on pg_store) and pass that value
into BuybackListResponse (keep the existing paging using items/page and mapping
via _buyback_to_response).
- Around line 181-197: reject_payout currently persists the updated record with
pg_store.persist_payout but never updates the in-memory cache, mirroring the
same bug in approve_payout; after the persist call in reject_payout, update the
module-level cache (e.g., _payout_store) with the new record (for example
_payout_store[payout_id] = record or update the existing entry's fields) so the
in-memory state matches the DB before returning the AdminApprovalResponse.
- Around line 242-259: The duplicate-check in create_buyback currently only
scans the latest 1000 records and does not handle DB UNIQUE violations; wrap the
call to pg_store.persist_buyback in a try/except that catches the DB integrity
exception (e.g., IntegrityError from your DB library) and raise
HTTPException(status_code=400, detail="Buyback already exists") when tx_hash
uniqueness is violated (referencing the BuybackTable.tx_hash constraint), then
continue to update _buyback_store[record.id] and return
_buyback_to_response(record) on success; keep the existing pre-check loop but
add this DB-level error handling around persist_buyback to handle duplicates
outside the scanned window or concurrent inserts.
- Around line 110-127: The pagination is incorrect because the code fetches a
bounded slice via pg_store.load_payouts(limit=limit + skip + 1000) then applies
filters in-memory (variables items, recipient, status, bounty_id, token,
start_date, end_date) and sets total = len(items), which only counts the
filtered subset within the fetch window; fix by moving filtering into the
database layer and using a proper count query: extend or replace
pg_store.load_payouts to accept filter parameters (recipient, status, bounty_id,
token, start_date, end_date) and an offset/limit, then call a new
pg_store.count_payouts(filters) to get the true total, and finally retrieve the
paged rows with pg_store.load_payouts(filters, limit, skip) so total reflects
the DB count and page contains correct records.
- Around line 272-294: The four service functions get_total_buybacks,
get_total_paid_out, _count_confirmed_payouts, and _count_buybacks currently call
pg_store.load_buybacks/load_payouts with limit=10000 which truncates results;
replace in-memory aggregation with database-side aggregates by adding and
calling new pg_store methods (e.g., sum_buybacks(),
sum_confirmed_payouts_by_token(token), count_confirmed_payouts(),
count_buybacks()) that run SQL SUM/COUNT queries (with proper WHERE for
PayoutStatus.CONFIRMED and token="FNDRY"/"SOL"), then update get_total_buybacks,
get_total_paid_out, _count_confirmed_payouts, and _count_buybacks to await those
pg_store aggregate methods and return the aggregated numbers.
- Around line 61-94: The duplicate-prevention in create_payout is racy and
incomplete: stop relying on load_payouts(limit=5000) and the in-memory
_payout_store scan and instead enforce uniqueness at the DB and handle conflicts
atomically—add DB unique constraints/indexes for tx_hash (unique if non-null)
and for bounty_id (unique or unique partial depending on nullable semantics),
modify pg_store.persist_payout to perform the insert and surface
IntegrityError/unique-constraint violations, and in create_payout call
persist_payout directly (or call a new pg_store.insert_payout_if_not_exists) and
catch the DB integrity exception to raise HTTPException(400) for duplicate
tx_hash or bounty_id; keep the in-memory _payout_store updates as caching only,
and remove/replace load_payouts usage in create_payout (references:
create_payout, pg_store.load_payouts, pg_store.persist_payout, PayoutRecord,
PayoutCreate).

In `@backend/app/services/pg_store.py`:
- Around line 276-280: The save_last_sync function currently always inserts a
new SyncStateTable row causing unbounded table growth; change save_last_sync to
update or upsert the single canonical sync row instead of inserting every time:
locate save_last_sync and replace the unconditional
session.add(SyncStateTable(last_sync=dt)) with an upsert/update flow (e.g.,
query for the existing SyncStateTable row by its unique key or primary id and
update its last_sync, or use the ORM/session.merge or DB-level ON CONFLICT
upsert to set last_sync = :dt), or alternatively delete old rows before
inserting so only one row remains; ensure this uses get_db_session() and commits
as before.

In `@backend/submit_bounty.py`:
- Around line 9-22: Remove the unused hardcoded pr_body string in
submit_bounty.py: delete the dead variable definition (pr_body) and any
unreachable block that defines it, ensuring no operational metadata or wallet
identifiers remain in backend source; if the text is required for documentation,
move it to a safe external doc (e.g., a markdown changelog) or a non-production
config, and run tests to confirm no references to pr_body remain.
- Around line 27-33: The script uses unsafe git operations: the subprocess.run
calls that execute ["git", "add", "."] and ["git", "push", "origin",
"bounty-169-final-certified", "--force"] in submit_bounty.py should be changed
to stage only intended files (avoid "git add ."), determine or accept the target
branch dynamically instead of hardcoding "bounty-169-final-certified", and avoid
--force; update the code around the subprocess.run invocations (the git
add/commit/push calls) to accept a whitelist or explicit file list, use the
current branch name or a function parameter for the target branch, and replace
--force with a safe push strategy (e.g., normal push or --force-with-lease)
and/or require confirmation before destructive pushes.
- Around line 1-38: This file backend/submit_bounty.py is an unrelated
git-automation script (contains run(), logger, and git subprocess calls) and
must be removed or relocated out of the backend FastAPI package: either delete
backend/submit_bounty.py from this branch/PR, or move it into a dedicated
tooling/automation repo or a top-level tools/ directory (preserving run() if you
need the script), then amend the commit(s) to remove the file from PR `#797`,
ensure no imports/reference to submit_bounty.py remain, update any CI or
packaging manifests so it isn't packaged with the backend, and run the test
suite/linters to confirm nothing breaks.

In `@backend/tests/e2e/conftest.py`:
- Around line 87-95: The code installs and reuses a global event loop at import
time via _test_loop/_get_test_loop and calls asyncio.set_event_loop and runs
async schema initialization before pytest fixtures, causing cross-loop pollution
with pytest-asyncio and never closing the loop; fix by removing import-time loop
creation and asyncio.set_event_loop usage, move any async schema initialization
into an async pytest fixture (or use pytest-asyncio's built-in
event_loop/async_client fixtures) so initialization runs on pytest-managed
loops, ensure the created loop (if any) is closed and cleared in teardown, and
update references from _get_test_loop/_test_loop to rely on the pytest event
loop instead of a global loop.
- Around line 30-33: The test harness currently forces a single temp-file SQLite
DB by unconditionally setting TEST_DB_PATH and os.environ["DATABASE_URL"] and
calling Base.metadata.create_all(), which reuses stale schema/data and prevents
running tests against PostgreSQL (breaking Mission `#82` validation); change
conftest.py so DATABASE_URL is only set to the tempfile path when no
DATABASE_URL exists in env (respect existing env to allow PostgreSQL), avoid
unconditionally calling Base.metadata.create_all() for production migrations
(use a per-test ephemeral DB or transactional fixtures and ensure tests truncate
tables or remove the temp file between runs), and add teardown logic to delete
TEST_DB_PATH or drop all tables after tests complete; refer to TEST_DB_PATH,
DATABASE_URL, and Base.metadata.create_all() to locate where to implement these
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ccc03247-c012-42f5-aea3-239b74812f03

📥 Commits

Reviewing files that changed from the base of the PR and between eafd2a8 and 560b781.

📒 Files selected for processing (5)
  • backend/app/services/payout_service.py
  • backend/app/services/pg_store.py
  • backend/submit_bounty.py
  • backend/tests/e2e/conftest.py
  • backend/verify_middleware.py

Comment on lines +186 to +190

record.status = PayoutStatus.FAILED
record.admin_approved_by = admin_id
record.failure_reason = reason
record.updated_at = datetime.now(timezone.utc)
await pg_store.persist_payout(record)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what fields persist_payout writes vs what PayoutRecord/PayoutTable have

echo "=== Fields written by persist_payout ==="
rg -A 20 'async def persist_payout' backend/app/services/pg_store.py | grep -E '^\s+\w+=' | head -20

echo ""
echo "=== PayoutTable columns ==="
ast-grep --pattern 'class PayoutTable($_) {
  $$$
}'
rg -A 40 'class PayoutTable' backend/app/models/tables.py | grep -E '^\s+\w+\s*=\s*Column'

echo ""
echo "=== PayoutRecord fields ==="
rg -A 50 'class PayoutRecord' backend/app/models/payout.py | grep -E '^\s+\w+:' | head -20

Repository: SolFoundry/solfoundry

Length of output: 1781


🏁 Script executed:

#!/bin/bash

# Get complete PayoutTable definition
echo "=== Complete PayoutTable definition ==="
rg -A 30 'class PayoutTable' backend/app/models/tables.py

echo ""
echo "=== Complete PayoutRecord definition ==="
rg -A 30 'class PayoutRecord' backend/app/models/payout.py

echo ""
echo "=== payout_service.py lines 186-230 (context around the issue) ==="
sed -n '186,230p' backend/app/services/payout_service.py

echo ""
echo "=== persist_payout full implementation ==="
rg -A 40 'async def persist_payout' backend/app/services/pg_store.py

Repository: SolFoundry/solfoundry

Length of output: 5912


failure_reason and updated_at are set but never persisted to PostgreSQL.

Lines 188–189 in reject_payout set record.failure_reason and record.updated_at, and lines 224, 227, and 231 in process_payout do the same. However, persist_payout only upserts 10 fields: recipient, recipient_wallet, amount, token, bounty_id, bounty_title, tx_hash, status, solscan_url, and created_at.

Critically, the PayoutTable schema itself has no updated_at or failure_reason columns—these fields exist only on PayoutRecord in memory. When either function calls persist_payout, the in-memory updates are lost, and load_payouts cannot reconstruct them from the database.

This causes rejected/failed payouts to have NULL failure reasons when reloaded, and all status transition timestamps are lost. The issue affects all three persist_payout calls: after rejection, after status transition to PROCESSING, and after on-chain transfer completion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/services/payout_service.py` around lines 186 - 190, The
PayoutRecord fields failure_reason and updated_at are only set in memory by
reject_payout and process_payout but never persisted because persist_payout (and
the PayoutTable schema) don't include those columns; update the DB
schema/migration to add failure_reason (text) and updated_at (timestamp with
timezone) to PayoutTable, update persist_payout to upsert these two fields, and
ensure load_payouts maps those columns back into PayoutRecord so rejected/failed
payouts retain their failure_reason and timestamps; touch the functions
persist_payout, PayoutTable definition, and load_payouts to implement this
change.

Comment on lines +44 to +45
from app.services.websocket_manager import manager as ws_manager, WebSocketManager, InMemoryPubSubAdapter # noqa: E402
from tests.e2e.factories import DEFAULT_WALLET # noqa: E402
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the conftest.py file to see the imports and fixture setup
fd conftest.py -t f | head -20

Repository: SolFoundry/solfoundry

Length of output: 120


🏁 Script executed:

# Let's find and read the conftest.py file
find . -name conftest.py -path "*/tests/e2e/*" -type f

Repository: SolFoundry/solfoundry

Length of output: 96


🏁 Script executed:

# Check if the file exists at the expected location
ls -la backend/tests/e2e/conftest.py 2>/dev/null || echo "File not found"

Repository: SolFoundry/solfoundry

Length of output: 144


🏁 Script executed:

# Find any conftest.py files
git ls-files | grep conftest

Repository: SolFoundry/solfoundry

Length of output: 120


🏁 Script executed:

# Read the e2e test conftest file
cat -n backend/tests/e2e/conftest.py | head -100

Repository: SolFoundry/solfoundry

Length of output: 4582


🏁 Script executed:

# Read more of conftest.py to see the fixtures and clear_stores() function
cat -n backend/tests/e2e/conftest.py | sed -n '100,200p'

Repository: SolFoundry/solfoundry

Length of output: 4907


🏁 Script executed:

# Read the rest of conftest.py
cat -n backend/tests/e2e/conftest.py | sed -n '200,350p'

Repository: SolFoundry/solfoundry

Length of output: 5737


🏁 Script executed:

# Also read the websocket_manager.py to understand the state
cat -n backend/app/services/websocket_manager.py | sed -n '1,100p'

Repository: SolFoundry/solfoundry

Length of output: 4363


🏁 Script executed:

# Read more of websocket_manager.py to understand the state variables and manager structure
cat -n backend/app/services/websocket_manager.py | sed -n '100,250p'

Repository: SolFoundry/solfoundry

Length of output: 5939


🏁 Script executed:

# Continue reading to find the shutdown method and confirm state variables
cat -n backend/app/services/websocket_manager.py | sed -n '250,400p'

Repository: SolFoundry/solfoundry

Length of output: 6704


🏁 Script executed:

# Check if ws_manager is used in the app routes
grep -r "ws_manager\|websocket_manager" backend/app/api/ --include="*.py" | head -20

Repository: SolFoundry/solfoundry

Length of output: 337


🏁 Script executed:

# Check the websocket router to see which manager instance is used
cat -n backend/app/api/websocket.py | head -100

Repository: SolFoundry/solfoundry

Length of output: 3843


🏁 Script executed:

# Search for shutdown calls in test conftest files
grep -r "shutdown" backend/tests/ --include="*.py"

Repository: SolFoundry/solfoundry

Length of output: 47


🏁 Script executed:

# Check how the manager singleton is instantiated
grep -n "^manager\s*=" backend/app/services/websocket_manager.py

Repository: SolFoundry/solfoundry

Length of output: 97


🏁 Script executed:

# Check if there are WebSocket E2E tests that use the manager
grep -r "websocket\|ws\|manager" backend/tests/e2e/ --include="*.py" | grep -E "(def test_|async def test_)" | head -20

Repository: SolFoundry/solfoundry

Length of output: 959


🏁 Script executed:

# Search for test files that might test WebSocket functionality
find backend/tests/e2e -name "*.py" -type f | xargs grep -l "websocket\|/ws"

Repository: SolFoundry/solfoundry

Length of output: 137


🏁 Script executed:

# Check if there's a lifespan or startup/shutdown in the test app
cat -n backend/tests/e2e/conftest.py | sed -n '150,175p'

Repository: SolFoundry/solfoundry

Length of output: 1117


🏁 Script executed:

# Read the WebSocket test file to see how tests use the manager
cat -n backend/tests/e2e/test_websocket_events.py | head -150

Repository: SolFoundry/solfoundry

Length of output: 6414


🏁 Script executed:

# Check if websocket_manager fixture is actually used in tests
grep -A 5 "def test_" backend/tests/e2e/test_websocket_events.py | head -80

Repository: SolFoundry/solfoundry

Length of output: 3229


🏁 Script executed:

# Look at the route-level WebSocket tests that use client fixture
cat -n backend/tests/e2e/test_websocket_events.py | grep -A 30 "def test_websocket_connect_and_subscribe"

Repository: SolFoundry/solfoundry

Length of output: 1644


🏁 Script executed:

# See all route-level tests (using client) vs unit tests (using websocket_manager fixture)
grep -E "def test_.*\(self, (client|websocket_manager)" backend/tests/e2e/test_websocket_events.py | head -30

Repository: SolFoundry/solfoundry

Length of output: 550


🏁 Script executed:

# Check what the client-based tests actually test
cat -n backend/tests/e2e/test_websocket_events.py | sed -n '200,350p'

Repository: SolFoundry/solfoundry

Length of output: 6543


🏁 Script executed:

# Check if there are route-level tests that might be affected
grep -n "def test_websocket" backend/tests/e2e/test_websocket_events.py | tail -20

Repository: SolFoundry/solfoundry

Length of output: 485


🏁 Script executed:

# Verify the actual singleton manager is used by the app and not overridable
grep -n "manager\|ws_manager" backend/app/api/websocket.py

Repository: SolFoundry/solfoundry

Length of output: 533


🏁 Script executed:

# Check if manager.shutdown() is ever called anywhere
grep -rn "shutdown()" backend/tests/ --include="*.py"

Repository: SolFoundry/solfoundry

Length of output: 47


WebSocket singleton state is not reset by clear_stores(), causing test isolation failures for route-level E2E tests.

The singleton manager imported at line 44 and used directly by routes in backend/app/api/websocket.py (lines 28, 31, 35, 42, 78, 115) retains _connections, _subscriptions, _rate_buckets, and _event_buffer between tests. The clear_stores() fixture (lines 173-188) only resets service-specific stores and never calls manager.shutdown(), which is the only method that clears singleton state (websocket_manager.py lines 197-209).

The websocket_manager fixture at lines 264-273 creates a separate instance and does not affect the singleton used by the actual routes. This leaves route-level WebSocket tests—test_websocket_connect_and_subscribe, test_websocket_reject_invalid_token, test_websocket_ping_pong, test_websocket_subscribe_and_broadcast, test_websocket_events_status_endpoint, test_websocket_events_type_endpoint—vulnerable to cross-test contamination from prior connection or subscription state.

Fix: Add await manager.shutdown() to clear_stores() to reset the singleton before each test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/tests/e2e/conftest.py` around lines 44 - 45, The clear_stores()
fixture currently resets service stores but misses resetting the WebSocket
singleton; modify clear_stores() to call and await manager.shutdown() (the
imported singleton `manager` from websocket_manager) so that
_connections/_subscriptions/_rate_buckets/_event_buffer are cleared before each
test, ensuring route-level E2E tests use a fresh WebSocketManager instance; no
changes to the separate websocket_manager fixture are needed.

Den A Ev added 30 commits March 23, 2026 19:42
The UNIQUE constraint on tx_hash was failing because previous test data
persisted in the shared SQLite database. Added real table deletion to
clear_stores fixture.
…aware dates, robust UUID handling, and test-compatibility fixes (PR SolFoundry#797)
…configuration, and stabilize test environment (PR SolFoundry#797)
…ructure, fix backend test leakage, and clear Ruff lint
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant