> ## Documentation Index
> Fetch the complete documentation index at: https://docs.walletwall.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Portfolio assessment cost model

# 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](../architecture/wallet-risk-report-model.mdx). 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`'s
  `fetchWalletRiskReport`) once per wallet, sequentially. It does not call
  `composeWalletRiskReport()` 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.md`
  and `docs/operations/dune-spend-path-controls.md`. Nothing here bypasses the
  `wallet-risk-report` route'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`/`readOrCache` path (`api/_dune.js`); Portfolio Assessment does
  not touch that path directly and adds zero new `DUNE_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` + `txlist` run concurrently, `tokentx` after).
* Alchemy: "Two Alchemy calls only" (`getTokenBalances` + `eth_getCode`), reduced
  specifically to ease 429 throttling.

Those numbers are the codebase's own definition of "the edge of safe" for **one**
wallet. If Portfolio Assessment ran two wallets concurrently, each with its own
internal 2-way fan-out, the batch would put roughly **double** that provider pressure
on Etherscan/Alchemy system-wide — silently exceeding a ceiling the code's own authors
already judged as the limit. Running one wallet at a time keeps total in-flight
pressure at, not above, that existing ceiling. This is a correctness constraint, not a
tunable — it does not change if the submission cap changes.

## The real per-hour ceiling: the existing route's own rate limit

`api/wallet-risk-report.js` already rate-limits itself to:

```
RATE_LIMIT_OPTIONS = { limit: 30, windowSeconds: 3600 }  // 30 requests / hour, per client IP
```

This budget is shared with **every** other caller of that route from the same IP
(Whale Watcher, Quantum, Wallet Profile entry points, and any other open tab). It is
also **not cache-aware at the route level** — like every other rate-limited route in
this codebase (`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 moment `fetchWalletRiskReport` reports
  `status: '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
  `retryAfterSeconds` as 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:

1. Accepts up to 100 submitted wallets (satisfying the UX requirement).
2. Processes them sequentially at concurrency 1 (satisfying the provider-safety
   evidence above).
3. 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."
4. Requires an explicit human click to continue past a pause — never auto-retries.

If a future revision wants all 100 wallets to complete faster than the existing
per-IP budget allows, that requires a deliberate, separately-reviewed decision (e.g. a
dedicated rate-limit bucket sized and approved for this route), not a client-side
workaround.
