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.

ColumnNotes
idPK
created_atenqueue time
chaine.g. bsc
contract_addressthe target
statuspending | running | done | error
errorpopulated on failure
requested_bywho enqueued
analyzed_atset on completion
claimed_atset 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_request
SET 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:

  1. Did the assembled source fit the 200k budget?
  2. 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_review only 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.

  • Plan: docs/superpowers/plans/2026-06-04-adhoc-analysis.md
  • Spec: docs/superpowers/specs/2026-06-04-adhoc-analysis-design.md

The 8 review findings fixed in the final pass:

  1. the stale-row reaper (added)
  2. drain exception safety (per-request error marking)
  3. the --limit 50 sentinel bug
  4. RateLimitError ordering
  5. stale existing dict during drain
  6. double Etherscan fetch in the open-ended path
  7. --dry-run --address guard
  8. live-test hygiene

Status / pending handoffs

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.

  • 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.
  • LOG
  • TOPICS