On-demand exploit analysis of ANY contract by address, plus a durable request queue (public.bsc_analysis_request) so the CRM can enqueue analyses and watch for results. Shipped 2026-06-04, commit 272a224, pushed to main. Extends the daily batch exploit stage (exploit-stage-deploy-handoff) without touching its result schema.
For Agents
Three entrypoints now funnel through ONE extracted function analyze_contract(cfg, dsn, cw, existing, run_label): (1) the daily batch loop, (2) python -m scanner.exploit --address 0x… [--chain bsc], (3) python -m scanner.exploit --drain-requests [--limit N]. Results for all three upsert to the SAME public.bsc_contract_analysis table the CRM verdict panel already reads (joined on contract_address) — no new result schema. The queue worker is not live until @deploy adds a second systemd timer; until then enqueued rows sit pending.
What shipped
--address — analyze any contract on demand
python -m scanner.exploit --address 0x… [--chain bsc] analyzes an arbitrary address, regardless of whether it came out of the daily funnel.
If the address has High Slither findings in the latest bsc_finding batch → Claude judges those findings (identical path to the daily stage).
If the address has no findings → Claude runs an open-ended source audit. This is a new prompt variant: “no static-analysis findings provided, audit for exploitable vulns.” It is not a finding-by-finding triage but a from-scratch review of the fetched source.
Result upserts to bsc_contract_analysis with run_label='adhoc'.
--drain-requests — process the queue
python -m scanner.exploit --drain-requests [--limit N] drains the new bsc_analysis_request queue, processing pending rows one at a time through analyze_contract.
DRY refactor
All three entrypoints (daily batch, --address, queue drain) call the single extracted analyze_contract(cfg, dsn, cw, existing, run_label). The run_label differs (batch-YYYY-MM-DD vs adhoc); the analysis logic is shared.
New table — public.bsc_analysis_request
Applied via the Supabase MCP. The queue, NOT a result table — results still land in bsc_contract_analysis.
Column
Notes
id
PK
created_at
enqueue time
chain
e.g. bsc
contract_address
the target
status
pending | running | done | error
error
populated on failure
requested_by
who enqueued
analyzed_at
set on completion
claimed_at
set on atomic claim (used by the stale-row reaper)
RLS posture
authenticated role can insert + select (so the CRM can enqueue requests and watch their status). The worker connects as the pooler postgres role, which bypasses RLS — it can claim/update any row. This mirrors the established connection rule (Session Pooler :5432, see exploit-stage-deploy-handoff).
Queue-worker invariants (durable design)
Concurrency-safe atomic claim
Claiming a row uses FOR UPDATE SKIP LOCKED so multiple workers never grab the same row:
UPDATE bsc_analysis_requestSET status='running', claimed_at=now()WHERE id = ( SELECT id FROM bsc_analysis_request WHERE status='pending' ORDER BY created_at LIMIT 1 FOR UPDATE SKIP LOCKED)RETURNING …
Stale-row reaper at drain start
Before draining, rows stuck in running with claimed_at < now() - interval '30 minutes' are requeued to pending. This recovers from killed processes — a worker that died mid-analysis would otherwise strand its claimed row in running forever.
Per-request exception safety
Any failure during a request best-effort marks the row error (a nested try/except swallows secondary DB failures so the cleanup itself can’t strand the row). A claimed row therefore never stays in running after the worker moves on. A Claude rate-limit re-queues the in-flight row to pending and stops the drain cleanly — it will be retried on the next drain.
Shared result schema
Results land in the SAME bsc_contract_analysis table the daily stage writes and the CRM verdict panel reads (joined on contract_address). No new result schema, no second sink to maintain.
Bundled bug fix — the needs_review gotcha
build_source now returns (text, truncated), not (text, complete)
The old code conflated two unrelated facts:
Did the assembled source fit the 200k budget?
Did every Slither snippet match as an exact substring of the source?
When (2) failed, it emitted the misleading message “source too large to retain finding code” even when the full source was present. Concrete case: the two proxy contracts on 2026-06-03 were only 37–70k chars — well under the 200k budget — but got parked as needs_review with that wrong message.
Fix: snippet matching is now whitespace-normalized (so formatting differences don’t cause false misses), and a contract is marked needs_reviewonly when truncation ACTUALLY drops finding-bearing code, with an honest message. The return value flipped from complete to truncated to match the real semantic.
This supersedes the framing in Live validation — 2026-06-03, where the two needs_review proxy contracts were attributed to the source budget — the real cause was the substring-match conflation, now fixed.
Update 2026-06-04 — proxy case had a second root cause
The whitespace-normalization fix above handled snippet matching against a single source set, but the proxy contracts had a distinct root cause: the exploit stage was analyzing only the implementation source while Slither had flagged the proxy’s own code, so the flagged snippets weren’t present at all (not a budget issue). See proxy-source-merge-gotcha for the fetch_analysis_source proxy+impl merge that fixes that. With both fixes, 0x9334 re-ran to false_positive and bsc_contract_analysis reached 0 needs_review.
Process
Built via brainstorm → writing-plans → subagent-driven TDD → two-stage review (spec-compliance + code-quality) → a fix pass addressing 8 review findings, then 37 tests pass.
Committed + pushed ( 272a224), but the queue worker is not live yet
Two cross-team handoffs were posted 2026-06-04, both awaiting the other agents:
@deploy — add a SECOND systemd timer running --drain-requests every ~5–10 min (frequent, lightweight) alongside the existing daily 06:00 batch timer. New pin: 272a224.
@crm — an “Analyze this contract” action that inserts into bsc_analysis_request, watches status, and refetches the verdict panel on done.
Until @deploy’s drain timer lands, enqueued requests sit pending — the worker isn’t running.
Related
exploit-stage-deploy-handoff — the daily batch exploit stage this extends; shares the analyze_contract core, the bsc_contract_analysis result table, and the Session-Pooler DSN rule. The needs_review fix here supersedes that note’s 2026-06-03 source-budget explanation.
contract-scanner — the 5-stage pipeline; on-demand analysis is a new front door to the LLM analysis layer downstream of triage.
supabase-findings-store — the bsc_finding store the finding-aware path reads from to find a contract’s High findings.