Portfolio Assessment — cost model & safe supported limit
Portfolio Assessment (/portfolio-assessment) is a controlled-pilot workspace that lets
someone paste or upload 10-100 public Ethereum wallets and get back an aggregated view
built entirely from each wallet’s existing, canonical
Wallet Risk Report. This document
records the actual provider/rate-limit evidence the feature’s concurrency and pacing
decisions are based on, and states plainly where the honest ceiling is lower than the
100-wallet submission cap.
What this feature adds, and what it deliberately does not add
- No new server endpoint. The workspace calls the existing, already-shipped
GET /api/wallet-risk-report?address=route (src/lib/wallet-risk-report-client.js’sfetchWalletRiskReport) once per wallet, sequentially. It does not callcomposeWalletRiskReport()in-process and does not add a batch API route. - No new provider call path. Every upstream call this feature triggers already
exists and is already governed by
docs/operations/provider-usage-governance.mdanddocs/operations/dune-spend-path-controls.md. Nothing here bypasses thewallet-risk-reportroute’s own rate limiter — the workspace simply calls it and honors whatever it reports back (including 429s). - No Dune execution or result-read code added. The Quantum/Dormancy dimensions
a Wallet Risk Report already includes are Dune-sourced through the existing
gated
getOrCache/readOrCachepath (api/_dune.js); Portfolio Assessment does not touch that path directly and adds zero newDUNE_QUERY_*references.
Why concurrency is 1, not 2 or more
api/_wallet-live-provider.js documents two provider-specific self-imposed ceilings for
a single wallet’s own internal fan-out:
- Etherscan: “cap peak concurrency at 2 to reduce self-inflicted pressure on the rate
limit” (
balance+txlistrun concurrently,tokentxafter). - Alchemy: “Two Alchemy calls only” (
getTokenBalances+eth_getCode), reduced specifically to ease 429 throttling.
The real per-hour ceiling: the existing route’s own rate limit
api/wallet-risk-report.js already rate-limits itself to:
api/wallet.js’s 30/300s, api/wallet-expand.js’s 20/300s), a request
counts against the budget regardless of whether the wallet turns out to be a warm
cache hit downstream. Portfolio Assessment inherits this behavior unmodified rather
than inventing a cache-aware credit scheme, to stay consistent with how every other
route in this app already works and to avoid adding a new rate-limiting design that
would need its own security review.
Consequence: in one hour, from one browser/IP, at most ~30 fresh
GET /api/wallet-risk-report calls can succeed — regardless of how many wallets were
submitted. A 100-wallet portfolio (assuming no cache warmth and no other concurrent
usage of the route from the same IP) will complete roughly 30 wallets before the route
starts returning 429s.
What the workspace actually does about it
- Submission cap: up to 100 distinct (post-dedup) wallets may be submitted, per the product requirement — this cap governs UX and never implies all 100 can finish in one sitting.
- De-duplication first: identical addresses (case-insensitive) collapse to one entry before any network work is scheduled — the single highest-leverage cost lever, per the provider-caching audit.
- Sequential dispatch, honest stop: the runner (
src/lib/portfolio-assessment/run-assessment.js) processes one wallet at a time. The momentfetchWalletRiskReportreportsstatus: 'rate-limited', the run stops immediately — it does not skip ahead, and does not retry. The remaining, not-yet-assessed wallets are kept queued. - Manual resume, never background polling: the UI shows the server-reported
retryAfterSecondsas a countdown and only enables a “Resume” button once it elapses — actually resuming still requires the person to click it. No timer ever fires a network request on its own. - Per-wallet failures never fail the batch: a network/server error for one wallet
(
status: 'unavailable') is recorded against that wallet only; the run continues to the next one.
Documented blocker
Safely supporting a full 100-wallet run in a single sitting is not possible under the current per-IP rate-limit contract on/api/wallet-risk-report — that is an
existing, deliberate product decision on a shared route, not something this feature is
positioned to change unilaterally. Rather than pretending otherwise, Portfolio
Assessment:
- Accepts up to 100 submitted wallets (satisfying the UX requirement).
- Processes them sequentially at concurrency 1 (satisfying the provider-safety evidence above).
- Honestly pauses at the real, server-reported rate-limit boundary rather than a guessed static number — because the 30/hour budget is shared with other app usage on the same IP, the number that will actually complete before a pause varies by session, and reacting to the live 429 is more honest than hard-coding “stop after 30.”
- Requires an explicit human click to continue past a pause — never auto-retries.

