Real-ground report. Everything below is pasted/derived directly from the read_app tool output.
vault-tracker v2, confirmed real. 2 scripts (refresh, readSnapshot). 1 cron (refresh, * * * * *). permissions: fetch:http, read:chain, read:appdata, write:appdata.
bugs I can now point to with real code references, not hand-waving:
bug 1 — holders KPI shows --
refresh.ts, this line:
const holdersCount = gtInfo?.data?.attributes?.holders?.count || gtInfo?.data?.attributes?.holders || null;
gt v2 /tokens endpoint (dry-run confirmed) does not return a holders field at all. attributes keys are: address, name, symbol, decimals, imageurl, coingeckocoinid, totalsupply, normalizedtotalsupply, priceusd, fdvusd, totalreserveinusd, volumeusd, marketcapusd. so holdersCount is always null → UI shows --. matches your flag.
bug 2 — top-20 panel: BALANCE = --, % SHARE = NaN%
refresh.ts writes each holder as:
{ rank, address, amount, percent, valueUsd, label, entityType, isPool }
index.html renderHolders reads:
const bal = Number(h.balance) / 1e18;
const pct = totalSupply ? (Number(h.balance) / Number(totalSupply)) * 100 : 0;
h.balance doesn't exist in the script output → Number(undefined) = NaN → "--" and "NaN%". real shape mismatch confirmed.
also: gt p1 amount field is already decimal-adjusted (dry-run returned 65233920485.44 for Uniswap PoolManager = 65.2B tokens, not raw wei). dividing by 1e18 would be wrong. totalSupply from gt v2 is the raw 18-dec string "100000000000000000000000000000" — also not directly comparable without scaling.
bug 3 — no pool filter
refresh.ts maps isPool but never filters. top holder on this token is Uniswap PoolManager at 65.2% (dry-run confirmed). it would dominate the panel.
bug 4 — "live · 53m ago" stale
refresh.ts has safe() wrappers on every fetch that log errors but swallow them. if every fetch is failing the snapshot still writes with empty data. no lastError surfaced to the UI. can't diagnose without a run log, but the silent-error pattern is real.
now on your locked choices:
top-10 fix — you picked (b): change UI to read h.amount and h.ownerpercent directly. that's the right call. h.amount is already decimal-adjusted, h.owner_percent is already a percent. zero scaling math needed. I'll filter pool holders in refresh.ts so the UI never sees them.
holders count — basescan API. endpoint is /api?module=token&action=tokenholderlist&contractaddress=...&apikey=... — requires API key, returns paginated list. to get count you'd have to page through all holders (expensive). the cheaper endpoint is /api?module=stats&action=tokensupply which doesn't give holder count either. actually, basescan's holder count is only shown on their web UI, not in the v1 public API on the free tier as a direct count endpoint.
honest correction before you go get the key: basescan's free API does not have a "return holder count" endpoint. the tokenholderlist endpoint requires PRO tier (etherscan v2 pro / $$$). so BASESCANAPIKEY on free tier won't solve this.
alternatives that actually work for aggregate holder count:
- alchemy getTokenHolders (free tier available, returns pageKey-paginated list — still page-count, but easier)
- moralis /erc20/{address}/holders (free tier, returns count directly) ← cleanest
- covalent/goldrush tokenholdersv2 with pagesize=1 returns totalcount in metadata ← also clean
recommend moralis: free tier, one call gives total count. set MORALISAPIKEY in secrets.
or, if you don't want another API key dependency right now, the honest UI fallback: rename the KPI label to "TOP HOLDERS" and show "20 tracked" (count of what we actually render). stop lying. upgrade later.
your call on that one before I write the patch.
standing by on:
1. moralis key path vs. "TOP HOLDERS · 20 tracked" honest fallback for the KPI
2. once you pick, I write the full patch (refresh.ts index.html) new vaultAlerts script, dry-run all three, then updateapp to v3
also: I have not yet dry-run the alert threshold logic or the secrets read path. those come next, after you pick the holders-count approach.