Last updated: 2026-05-18 10:30 UTC during iter 30 (README/doc checkpoint 6). The iter 26–30 work added the analytics + feedback loops surfaces (/analytics, /api/analytics/overview, the Dune package, /api/feedback) — see the new Analytics + Feedback Loops (iter 26–29) section below for the tester-facing walk-through. Iter 30 also caught and fixed a stale production build that was hiding iter 27/29 from the public app — see docs/testnet/iter30-stale-build-redeploy.md. This doc has hand-curated sections (Sibling Apps, Protocol Lane Hardening Status, Analytics + Feedback Loops, Frontend health, operator runbook). Do not re-run scripts/update-testnet-readme.py until its template is reconciled with those sections — the generator currently overwrites them.
- Branch:
main - Snapshot source: committed README + GitHub Actions history for this branch
- Package version:
0.2.0 - Autobuilder iteration:
6 - Chain: GoodDollar L2 Devnet (
42069configured,42069live) - Latest local block:
176824
- Frontend: HTTP 200 — https://goodswap.goodclaw.org
- Landing: HTTP 200 — https://goodclaw.org
- Explorer: HTTP 200 — https://explorer.goodclaw.org
- RPC HTTPS: HTTP 200 chainId=0xa455 — https://rpc.goodclaw.org
- Paperclip: HTTP 200 — https://paperclip.goodclaw.org
- CI: GitHub
CIwas green on latest pushedmainbefore this README generation; every push must re-check. - On-chain protocol smoke: 6/6 protocol smoke tx lanes green
- Frontend E2E: matrix workflow
Parallel Dapp Testscovers each dapp lane independently. - Deployment: devnet deployment workflow is
Deploy to Devnet. - Required before public testnet: persistent OP Stack chain, faucet, final canonical address sync, explorer indexing check, Dune dashboard/indexing.
Per-lane status after the iter 16–30 hardening pass. "Hardened" means the lane has named proof on the public app (or — for cross-protocol UBI work — named proof on devnet) and is ready to feed the release candidate manifest. "Deferred" means the slot was consumed by a blocker and the work moves to a later row.
| Lane | Iter | State | Evidence |
|---|---|---|---|
| Swap | 16 | ✅ hardened | frontend/src/lib/devnet.ts constants re-pointed at op-stack/addresses.json; swap happy-path + dust/error proof captured. |
| Perps | 17 | ✅ hardened | frontend/e2e/perps-journey.spec.ts full open/close green; on-chain PerpEngine.positions(...) receipt. |
| Predict | 18 | ✅ hardened | /predict market grid surfaces meaningful markets when on-chain seeds are empty; frontend/e2e/predict-journey.spec.ts green. |
| Lend | 19 (target) | ⏳ deferred | Iter 19's slot was consumed by the next dev clobber recurrence #3 fix (see Frontend health (iter 19)). Lane proof scheduled for a follow-up iteration. |
| Stable | — | ⏳ deferred | Same deferral as Lend; will be picked up alongside it. |
| Stocks | — | ✅ stable (existing) | /stocks HTTP 200, smoke matrix green from prior iterations; no new hardening in iter 16–19. |
| Portfolio / Claim | 21 | ✅ hardened | /portfolio HTTP 200; frontend/e2e/portfolio-journey.spec.ts proves wallet-state + balances + claim UX on the public lane (iter 21 also fixed the --dist-dir CLI flag blocker so the lane could be greened). |
| UBI fee truth source | 22 | ✅ shipped | docs/UBI-FEE-ACCOUNTING.md — canonical 14-route map from every protocol fee path into the UBI revenue tracker. |
| UBI integration proof I (Swap + Perps) | 23 | ✅ integration proven | test/integration/UBIFeeIntegrationProofSwapPerps.t.sol — routes 1–5 proven by event + balance-delta receipts (commit 2b30ad5). |
| UBI integration proof II (Predict + Lend + Stable + Stocks) | 24 | ✅ integration proven | test/integration/UBIFeeIntegrationProofPredictLendStableStocks.t.sol — routes 6–14 proven; all 14 routes now read ✅ integration proven in the accounting spec (commit 3f2806a). |
| Analytics address book | 26 | ✅ shipped | analytics/address-book.json + analytics/README.md — machine-readable chain/protocol/fee-route truth source derived from op-stack/addresses.json. |
| Public analytics dashboard | 27 | ✅ hardened | /analytics + /api/analytics/overview — KPIs (chain block, supply, UBI revenue, protocol activity) on the live app, restored to HTTP 200 in iter 30 after a stale-build redeploy. |
| Dune / indexing-request package | 28 | ✅ shipped | analytics/dune-package/ — SQL pack, INDEXING_MANIFEST.json, decoding cookbook; ready to hand to external indexers (Dune, Goldsky, Allium). |
| Tester feedback ingest | 29 | ✅ hardened | Floating "Feedback" button on every page → /api/feedback (rate-limited, 16 KiB body cap, schema-validated, redacted) → frontend/data/feedback.jsonl. Vitest 17/17 + Playwright 3/3 + live POST proof in iter 30. See docs/testnet/iter29-feedback-pipeline.md. |
| Stale-prod-build fix + doc checkpoint 6 | 30 | ✅ shipped | iter 30 product review caught the live /analytics + /api/feedback schema were running an outdated build. frontend/scripts/deploy.sh redeployed the production tree; BUILD_ID re-synced. Evidence: docs/testnet/iter30-stale-build-redeploy.md, checkpoint summary: docs/testnet/iter30-readme-doc-checkpoint-6.md. |
Cross-cutting infra hardening that landed alongside the lane work:
- Iter 18 BLOCKER — PM2 build-less-start fence.
frontend/scripts/pm2-launch-next.mjsrefuses to launchnext startif.next/is missing a manifest or contaminated by anext devtree. This prevents the third class of "HTML 200 but every chunk 500" outages. - Iter 19 BLOCKER —
next devclobber recurrence #3 closed.distDirisolation for Playwright + thegoodswap-watchdogPM2 process that probes/_next/static/chunks/*.jsevery 60 s and reloadsgoodswapafter a 3-failure streak. Full operator runbook in Frontend health (iter 19). - Iter 21 BLOCKER —
--dist-dirCLI flag unsupported. Removed the unsupported--dist-dirflag from the Playwright wrapper and switched to env-based isolation so the portfolio lane could be greened in the same iteration. - Iter 30 CRITICAL — stale production build. Iter 30's product review caught
/analytics(iter 27) returning HTTP 404 and/api/feedback(iter 29) running the old non-validating handler on the public app, while both passed locally. The fix was procedural — runfrontend/scripts/deploy.sh(npm ci && npm run build && pm2 reload goodswap --update-env && scripts/check-buildid-sync.mjs --strict) — but it confirms why non-negotiable #6 ("public URLs and production behavior matter more than localhost") has to be enforced every five iterations. Seedocs/testnet/iter30-stale-build-redeploy.md.
The Lend/Stable lane deferral is intentionally visible here so a tester reading this doc does not assume rows 19/20 of the 50-iteration plan mean those lanes have public-app proof yet — the UBI fee routing for Lend and Stable, however, is fully integration-proven (iter 24) even though the public-app lane proof remains deferred.
The iter 26–29 work turned the chain into something testers can actually look at and talk back to. Four loops, all live on the public app after the iter 30 redeploy:
analytics/address-book.json is the
machine-readable truth source for every chain ID, RPC, explorer,
protocol contract, and UBI fee route. It is derived from
op-stack/addresses.json, so when
contracts redeploy the address book follows automatically and external
indexers do not have to scrape source code or guess at deployment
broadcast files. See analytics/README.md.
The live app exposes a tester-facing dashboard at https://goodswap.goodclaw.org/analytics, backed by https://goodswap.goodclaw.org/api/analytics/overview. The page reads the address book and reports:
- Current chain block + chain ID
- G$ total supply
- UBI revenue tracker balance (the on-chain "how much fee revenue has reached UBI so far?" number)
- Recent protocol activity counters
The endpoint returns HTTP 200 JSON; the page renders the same numbers in a polished card layout. Iter 30's stale-build CRITICAL caught that this route was 404 on the public app between iter 27 and iter 30 — the iter 30 redeploy restored it. Do not skip the public-URL probe on future doc checkpoints.
analytics/dune-package/ is the
external-indexer onboarding kit. It contains:
INDEXING_MANIFEST.json— the contracts + ABIs + fee routes an external indexer needs to subscribe to.- SQL pack — a starter set of Dune queries against the manifest (network usage, protocol activity, UBI fee routing, agent economy, faucet funnel, success/revert rates).
- Decoding cookbook — protocol-by-protocol notes on event semantics and gotchas so an indexer can ship correct dashboards without reading the Solidity.
Use this when handing the chain off to Dune, Goldsky, Allium, or a custom indexer.
Every page now has a floating Feedback button in the bottom-right. Click it to open a modal that captures, automatically:
- Route + full URL the tester is on
- Connected wallet address (preserved — public identifier)
- Wallet chain ID
- Viewport size + user agent
- Anonymous
sessionId(for cross-page correlation; no PII) - Build SHA the production app is currently serving
- The last ≤ 20 console errors observed by
ConsoleErrorCapture(truncated to 500 chars each) - The tester's own
description+type(bug/ux/feature/other)
The form POSTs to /api/feedback, which is:
- Rate-limited (
withApiRateLimit). - Body-capped at 16 KiB before JSON parsing.
- Schema-validated against
FeedbackPayload— wrong field name, wrong type, or out-of-bounds value returns HTTP 400 with a per-field message. - Redacted on every string leaf by
redactDeep— hex private keys, 12/24-word BIP-39 mnemonics, JWTs,Bearer …tokens,password=/api_key=form/query fragments, and emails are replaced with[REDACTED]. Wallet addresses are intentionally preserved. - Persisted as one JSON line per record to
FEEDBACK_LOG_FILE(defaults tofrontend/data/feedback.jsonl, gitignored). Disk-write failures are logged but never bubble up — feedback never 5xx.
The on-disk JSONL stream is the operator triage queue. Tail it with:
tail -n 20 frontend/data/feedback.jsonl | jq .Tests pinning this contract: Vitest 17/17 in
frontend/src/app/api/feedback/__tests__/route.test.ts, Playwright 3/3
in frontend/e2e/feedback-button.spec.ts, plus a live production POST
proof in docs/testnet/iter30-stale-build-redeploy.md.
Full implementation notes (architecture diagram, redaction policy,
schema, persistence format, all proofs) live in
docs/testnet/iter29-feedback-pipeline.md.
These apps run on the same host and are publicly reachable but are explicitly out of scope for the testnet release gate. They share Caddy + PM2 with goodswap.goodclaw.org but their health is not counted in /api/status and a failure must not block a testnet tag.
| App | URL | Port | PM2 name | Source repo |
|---|---|---|---|---|
| GoodAgent prototype | https://goodagent.goodclaw.org | 3099 | goodagent-prototype |
/home/goodclaw/goodagent-prototype |
Rules per non-negotiable #8 ("Do not hide degraded services; fix them or document why they are intentionally excluded"):
- Excluded from
/api/status. The 12 backend services tracked by/api/statusare the only services that gate releases (swap-oracle,activity-reporter,harvest-keeper,liquidator,revenue-tracker,stocks-keeper,indexer,monitor,rpc-balancer,bridge-keeper,perps,predict). Sibling apps do not appear there and do not block the gate. - Still must not degrade the host. If a sibling app hot-loops (PM2 restart counter climbing, port held by an orphaned process, or
EADDRINUSEon its assigned port), it must be fixed or stopped — its noise cannot mask gate-relevant problems. See.autobuilder/test-evidence/iter15/goodagent-prototype-recovery/for the iter15 recovery from an orphanednext-serveron port 3099. - Port ownership. Each sibling app owns one port. The testnet gate owns port 3100 (frontend). If a sibling app collides with a gate port, the sibling app loses.
- Triage commands (when a sibling app is misbehaving):
If PM2's tracked PID and the port owner PID differ AND the port owner has PPID=1, kill the orphan (
pm2 describe <name> # restart count, uptime, current PID ss -tlnp 'sport = :<port>' # who actually holds the port ps -p <pid> -o pid,ppid,uid,etime,cmd # PPID=1 means orphan — kill it
kill <pid>), thenpm2 reset <name>to clear the restart counter.
- Status: included in the testnet move plan.
- Spec:
docs/DUNE-DASHBOARD-SPEC.md. - Launch requirement: public proof dashboard for network usage, protocol activity, UBI fee routing, agent economy, faucet funnel, and success/revert rates.
- Indexing note: local chain
42069uses internal indexer until the public testnet is Dune-indexed or Dune indexing is requested.
- GoodSwap:
0x52c01d13f34c…(✅ success) - GoodPerps:
0x44e23761376c…(✅ success) - GoodLend:
0x6cda4eb0312a…(✅ success) - GoodStable:
0x2306c5aef499…(✅ success) - GoodStocks:
0xa8325cba97be…(✅ success) - GoodPredict:
0x899ceb32b4a5…(✅ success)
The on-chain leg of the UBI promise — "every protocol fee routes to the
UBI revenue tracker" — is enumerated in
docs/UBI-FEE-ACCOUNTING.md (14 routes) and
proven on devnet by two integration tests:
| Protocols | Routes | Proof file | Iter / commit |
|---|---|---|---|
| Swap + Perps | 1–5 | test/integration/UBIFeeIntegrationProofSwapPerps.t.sol |
iter 23 (2b30ad5) |
| Predict + Lend + Stable + Stocks | 6–14 | test/integration/UBIFeeIntegrationProofPredictLendStableStocks.t.sol |
iter 24 (3f2806a) |
All 14 routes are marked ✅ integration proven in the accounting spec
as of iter 24. Both proof files exercise the real protocol fee paths
end-to-end and assert UBI revenue tracker balance deltas rather than
relying on unit-test mocks.
Run the proofs locally:
forge test --match-path 'test/integration/UBIFeeIntegrationProof*'The canonical sources of truth are:
op-stack/addresses.json— imported by the frontend (frontend/src/lib/devnet.ts)..autobuilder/addresses.env— sourced by deploy scripts, backend services, and tests.
Both files are regenerated from Foundry broadcast artifacts plus on-chain
bytecode by scripts/refresh-addresses.py. They are protected by two CI
gates that prevent silent drift:
Runs the full pipeline in memory and compares the result against the
files on disk. Exit code 0 means the registry matches broadcast+chain
truth; exit code 1 prints a unified diff of every byte that would
change. Run after any redeploy:
python3 scripts/refresh-addresses.py --checkIf it fails, drop --check to actually rewrite the files, then commit.
Walks frontend/src/ (override with --paths) for every hex address
literal of the form 0x[0-9a-f]{40} and fails on any address that is
neither:
- In the canonical registry above, OR
- On the bake-in allowlist (
0x0…0,0x…dead, the four Anvil dev wallets, the OP Stack predeploy range0x4200…00–0x4200…FF), OR - Tagged on the line itself or within 20 preceding non-blank lines
with one of:
STALE,hardcoded,redeploy, orallowlist:.
Run it as:
python3 scripts/check_no_stale_addresses.py
python3 scripts/check_no_stale_addresses.py --json # CI-friendlyThis is what blocks "we redeployed everything but the frontend still points at the old MarketFactory" from sneaking into a release.
Both gates are exercised by scripts/test_refresh_addresses.py.
- GoodDollarToken:
0x8f86403a4de0bb5791fa46b8e795c547942fe4cf - UBIFeeSplitter:
0x809d550fca64d94bd9f66e60752a544199cfac3d - UBIClaimV2:
0x9d4454b023096f34b160d6b654540c56a1f81688 - GoodSwapRouter:
0x975cdd867acb99f0195be09c269e2440aa1b1fa8 - SwapPriceOracle:
0x19ceccd6942ad38562ee10bafd44776ceb67e923 - PerpEngine:
0x084815d1330ecc3ef94193a19ec222c0c73dff2d - MarginVault:
0x82bbaa3b0982d88741b275ae1752db85cafe3c65 - MarketFactory:
0xfaA7b3a4b5c3f54a934a2e33D34C7bC099f96CCE - GoodLendPool:
0xcbeaf3bde82155f56486fb5a1072cb8baaf547cc - VaultManager:
0x3489745eff9525ccc3d8c648102fe2cf3485e228 - gUSD:
0xed12be400a07910e4d4e743e4cee26ab1fc9a961 - CollateralVault:
0x276c216d241856199a83bf27b2286659e5b877d3 - SyntheticAssetFactory:
0xfaaddc93baf78e89dcf37ba67943e1be8f37bb8c - UBIRevenueTracker:
0xfd6f7a6a5c21a3f503ebae7a473639974379c351
Run lanes independently so one dapp failure does not hide the others:
- Contracts: Foundry build, unit tests, gas report, Slither audit scope.
- Swap: quote, approve, execute, price-impact guard, UBI fee routing.
- Perps: deposit margin, open/close position, liquidation threshold, API market feed.
- Predict: create market, buy/sell outcome, resolve, market detail UI.
- Lend: supply, withdraw, borrow/repay health-factor checks.
- Stable: deposit collateral, mint/repay gUSD, PSM swap, stability pool.
- Stocks: deposit collateral, mint/burn synthetic equity, oracle freshness.
- Portfolio/Claim: wallet states, balances, UBI claim, explorer links.
Public testers should never have to type RPC URLs into MetaMask by hand. The frontend now ships a one-click "Add GoodChain Testnet to wallet" button in two places:
/testnet-guide→ top of the "1. Add GoodChain Testnet" section, above the network table. Full-width CTA; success state offers an "Open Faucet →" follow-up link./faucet→ compact pill above the "Wallet Address" input, framed by a "First time here?" hint.
Implementation:
- Component:
frontend/src/components/AddNetworkButton.tsx - Unit tests:
frontend/src/components/__tests__/AddNetworkButton.test.tsx(8 specs, covers idle / compact / no-wallet / success / rejected / error states and asserts the canonical EIP-3085 payload). - E2E:
frontend/e2e/onboarding.spec.tsclicks the button on both pages, captures before/after screenshots infrontend/e2e/screenshots/, and asserts the wallet received the canonical payload. - Mock wallet:
frontend/e2e/fixtures/wallet.tsrecords everywallet_addEthereumChaincall onwindow.__addEthereumChainCallsso the spec can introspect what reached the wallet.
The button is wired to frontend/src/lib/devnet.ts, which sources
chain_id, rpc_url, and explorer_url directly from
op-stack/addresses.json. No hardcoded fallbacks — when the canonical
registry changes, the onboarding flow follows automatically.
EIP-3085 payload shape sent to the wallet:
{
"chainId": "0xa455",
"chainName": "GoodChain Testnet",
"rpcUrls": ["https://rpc.goodclaw.org"],
"blockExplorerUrls": ["https://explorer.goodclaw.org"],
"nativeCurrency": { "name": "GoodDollar", "symbol": "G$", "decimals": 18 }
}A manual "Or add it manually" panel remains on /testnet-guide for
wallets that do not implement EIP-3085 (hardware wallets, some mobile
wallets without injected providers).
The in-app guide (/testnet-guide) now includes a For developers section
with a copy-pasteable RPC reachability curl command and direct links to
op-stack/addresses.json, docs/ARCHITECTURE.md, and this README on GitHub.
The section appears in the sticky TOC under #for-developers.
The frontend production build is wrapped by frontend/scripts/atomic-build.mjs,
which builds into a temporary .next.tmp directory and atomically swaps it
in only on success. Partial or failed builds can no longer corrupt the
deployed .next/ directory. See docs/runbooks/frontend-rebuild.md for the
operator workflow.
goodswap (port 3100) is supervised by two layers:
- PM2 launcher (
frontend/scripts/pm2-launch-next.mjs) — refuses to startnext startif.next/is missing a manifest or has been clobbered by anext devtree (.next/static/development/). This is the "fail-fast on broken build" gate; see iter19 blocker task0029-iter19-blocker-playwright-clobber-recurrence-3-distdir-isolation.mdfor the full diagnosis of the recurrence. - Watchdog (
frontend/scripts/goodswap-watchdog.mjs, PM2 processgoodswap-watchdog) — the runtime probe. EveryPROBE_INTERVAL_MS(default 60s) it hitsPROBE_URL(defaulthttp://localhost:3100/), parses the served HTML for/_next/static/chunks/*.jsURLs, samples up tochunkSampleLimitof them, and HEADs each one. If the page is unreachable, returns non-200, or any sampled chunk 404s, the probe counts as a failure.
The watchdog never reloads on the first failure. It increments a streak
counter, requires the streak to reach FAILURE_THRESHOLD (default 3)
within the recent window, then calls pm2 reload goodswap and enters a
reloadCooldownMs window (default 5 min) before it will fire again.
A clean probe inside the recovery window resets the streak.
Why it exists. Three times now (iters 17/18/19) next dev clobbered
the production .next/ and the only signal was 404s on
/_next/static/chunks/*.js. The 200 on / alone was not enough — the
HTML shell loaded fine while the JS bundles were gone. The watchdog
probes the same chunk URLs check-served-chunks.mjs uses for one-shot
verification, so the recovery check used after atomic-build and the
runtime watchdog see the world the same way.
Config knobs (all env-driven, no rebuild required):
| Env var | Default | Meaning |
|---|---|---|
PROBE_URL |
http://localhost:3100/ |
URL to fetch + parse for chunk references |
PROBE_INTERVAL_MS |
60000 |
How often to probe |
FAILURE_THRESHOLD |
3 |
Consecutive failures before reload |
RELOAD_COOLDOWN_MS |
300000 |
Min gap between two reloads |
RECOVERY_DELAY_MS |
30000 |
Grace period after reload before reprobing |
PM2_TARGET |
goodswap |
PM2 process to reload |
WATCHDOG_LOG_FILE |
(unset) | If set, mirror every JSON event to file |
Start / stop / inspect:
# start (or reload if already running)
pm2 startOrReload frontend/ecosystem.watchdog.config.cjs
pm2 save
# live status + last 50 events
pm2 describe goodswap-watchdog
pm2 logs goodswap-watchdog --lines 50 --nostream
# file log (configured via WATCHDOG_LOG_FILE in the ecosystem)
tail -n 50 frontend/.autobuilder-logs/goodswap-watchdog.logDry-run mode (operator wants to simulate a failure without reloading production):
PROBE_URL=http://localhost:3999/ PROBE_INTERVAL_MS=300 \
node frontend/scripts/goodswap-watchdog.mjs --dry-runThis emits reload-start / reload-end events with dryRun:true and
exit code 0 instead of invoking pm2 reload, so the streak and cooldown
logic can be exercised against a deliberately unreachable port without
touching the live goodswap process.
Single-shot mode (--once) is used by smoke tests / CI:
node frontend/scripts/goodswap-watchdog.mjs --once
# exit 0 — current probe OK
# exit 2 — current probe failed (reason in stdout JSON)Manual recovery when the watchdog fires. If the watchdog reloads
goodswap and the next probe is still failing, the .next/ directory
is structurally broken — a pm2 reload alone cannot fix it. Run the
atomic rebuild from docs/runbooks/frontend-rebuild.md instead.
Production origin: https://goodswap.goodclaw.org
When deploying a new public origin (preview environment, staging,
new domain) the SDK will log a red Origin ... not found on Allowlist error and a [Reown Config] Failed to fetch remote project configuration warning on every page load until the origin
is added to the WalletConnect Cloud project.
To add the origin permanently (replaces the runtime suppression in
frontend/src/lib/wagmi.ts):
- Log into https://cloud.reown.com with the project owner account.
- Open the project that matches
NEXT_PUBLIC_WC_PROJECT_IDinfrontend/.env.local(last four chars:97d1). - Under Settings → Allowed origins, add the production URL (and any preview/staging URLs).
- Reload the app and confirm the console no longer logs the allowlist error even with the suppression filter removed.
The runtime suppression in wagmi.ts is a stopgap so the public
app ships clean while waiting for cloud-side access; it is
narrowly scoped to those two exact log patterns and silences no
other Reown logging.
- Group update: every 5 autobuilder commits/iterations via
autobuilder-progress-monitor. - This README: regenerate with
python3 scripts/update-testnet-readme.pybefore each 5-iteration push/deploy update. - After deploy: run
bash scripts/health-check.shandbash scripts/verify-onchain-integration.sh, then regenerate this file.