dash.foogy
entity updated 2026-07-28 live

agents4fun

Summary: One pnpm+Foundry monorepo (repo agent-wl, product “Agents 4.Fun”, powered by depunks) shipping TWO on-chain products — a fair Whitelist/allowlist tool and a Sweepstakes mint-as-ticket tool — both using Chainlink VRF v2.5 + Solady LibPRNG; no prize token is ever issued by the whitelist.

What it is

“Agents 4.Fun” is a multichain, on-chain fair-random-selection app where humans and AI agents apply through the same permissionless on-chain path. Two products live in the repo:

  1. Whitelist — a paid, token-gated allowlist tool; the draw picks winning wallets, exported off-chain (e.g. an OpenSea allowlist). It issues no rewards or token itself.
  2. Sweepstakes (“sweep”) — mint-as-ticket: each mint of a creator’s chosen NFT source is one ticket; once tickets cover a target NFT price, the pooled funds buy that NFT and a VRF draw picks the winning ticket.

Product name in the UI is literally “Agents 4.Fun” (apps/web/src/routes/landing.tsx); “Powered by DePunks (depunks.club)”.

Note: an earlier framing (“sweepstakes + NFTSweeps + our-own-PRNG”) was inaccurate vs the code. The whitelist uses Chainlink VRF v2.5 (not a homegrown PRNG for the seed), and the two products are distinct.

Stack / where it lives

  • Repo: ~/Documents/localhost/agent-wl (remote github.com/damir5/agent-wl); active branch dev.
  • pnpm 11 + Foundry monorepo, Node ≥ 24. README.md, CLAUDE.md
  • apps/web = TanStack Start + React 19.2 + Vite 8 + Tailwind v4 (wagmi/viem, RainbowKit, Better Auth SIWE, oRPC). apps/web/package.json
  • apps/backend = Effect 4 (single effect package) → /api (OpenAPI + HTTP MCP) and /rpc (oRPC). Keeper worker signs only operational txs, never user txs. README.md
  • apps/indexer = Ponder → Postgres read model. packages/db = Kysely (off-chain tables only). packages/config = ChainRegistry.
  • Chains: Ethereum mainnet, Base, Ethereum Sepolia, Base Sepolia (+ local Anvil 31337). CLAUDE.md
  • Two coexisting web skins (as of 2026-07-16): (1) the legacy .protocol-shell “dash.foogy.xyz style” token set (off-white, #111 text, #ff5a1f accent, flattened — no neumorphism) in apps/web/src/styles.css (~line 264), still worn by sweeps/whitelists/admin; and (2) the new .a4f-shell “joyful” consumer skin (apps/web/src/a4f.css) on the authenticated app (welcome/scan/profile). Both are separate from the DePunks design.md dark system. See “Joyful UI skin” below.

Product 1 — Whitelist (allowlist)

  • Token-gated entry via a required on-chain IEligibilityHook; built-in table-driven StandardHook gates by collection: ERC-721 per-token single-use, ERC-1155 up to held balance capped at a configured per-id supply. evaluate(...) → Grant[]{tier, useKey, capacity}, staticcall, 300k gas, fail-closed. docs/hook-abi.md
  • Tiers + per-wallet caps: ordered tiers (tier 0 highest), strict-priority draw; per-tier per-wallet caps enforced by the contract, not the hook. docs/eligibility-and-tickets.md
  • Draw: single Chainlink VRF v2.5 seed → Solady LibPRNG partial Fisher-Yates per tier, batched with a persisted cursor; ticket-level wins by default, optional one-win-per-wallet. README.md
  • Winner export: after the draw you export winning wallet addresses as CSV (e.g. an OpenSea allowlist), gated behind chain finality. RPC winnerCsvData; REQ-031 / REQ-127. docs/eligibility-and-tickets.md, docs/requirements.md
  • Fees: per-ticket entry fee + one-time setup fee, both non-refundable, swept to the project Safe in the collecting tx (default 0.01 ETH). Contract never holds fee balances. README.md
  • No prize pool / no token: the system issues no rewards; winners just land on the exported list. Optional owner-at-pick mode (ERC-721 only) reassigns a win to the token’s current owner at draw time. README.md
  • Contract: WhitelistRouter (UUPS, per-whitelistId mappings, Safe-owned) + StandardHook; 38 forge tests. README.md

Product 2 — Sweepstakes (“sweep”, mint-as-ticket)

Contracts under packages/contracts/sweep/src/.

  • Mechanic: a sweep is bound to a mint source (mintContract + mintDeployBlock) at deploy. Mints are indexed (Ponder) → each mint = 1 ticket, funded on-chain via keeper-signed EIP-712 FundingBatch (fundTickets). A round has a targetPrice (the live NFT ask); once accumulated ticket value covers it, the keeper calls attemptPurchase, which atomically buys the target NFT, then a VRF v2.5 draw picks the winning ticket. Overflow rolls to the next round (multi-round). SweepMultiRoundBase.sol, base/RoundStorage.sol
  • SweepFactory (UUPS, Ownable): deploys strategy-specific sweep proxies (deployNFTStrategySweep / deployPunkStrategySweep), owns the single VRF v2.5 subscription, holds VRF config, keeper, 4 team wallets + special-fee wallet. SweepFactory.sol
  • Strategies: SweepMultiRound_NFTStrategy (generic ERC-721 via a marketplace adapter) and SweepMultiRound_PunkStrategy (buys CryptoPunks directly via buyPunk/transferPunk, requires exact price match since punks predate ERC-721). SweepMultiRound_PunkStrategy.sol, test/unit/SweepPunkStrategy.t.sol
  • Fee split (hardcoded BPS): 90% to round balance, 2% split across 4 team wallets, 8% to special-fee wallet; non-refundable. .fdb/domain-language/contract-fee-split.md
  • Note: the sweep product was merged into agent-wl (remote branch merge/sweepstakes-into-aw); it originated from a separate sweepy repo.

Agent surface

  • /api is the stable public agent/HTTP-MCP surface (discovery, whitelist reads, unsigned-tx construction). Agents sign and broadcast their own txs; the backend never signs applicant txs. README.md
  • Agent discovery is now admin-managed, not runtime-discovered (see below). The former daily pnpm agents:snapshot job (enumerated ERC-8004-bound agents across OpenSea agent-category collections) has been retired in favour of an admin-curated collection registry. Still ties into opensea-erc8217 / depunks.

Wallet Score — off-chain daily reputation (SHIPPED 2026-07-09 → 07-10)

A per-wallet trust score that mirrors depunks’s reputation-not-escrow philosophy (agent-rep-trust) at the participant level.

This section describes the original v5 formula (normalized weighted sum). It was superseded by the collector-reputation reframe and fixed-base rewrite — see “Wallet Score formula overhaul v6→v8” below. The population/refresh policy and the “reputation not escrow” framing still hold; the formula math and the identity-bonus/normalized-denominator mechanics do not.

  • One integer 0–100 score per (wallet, chain_id), plus a band (low <30, high ≥65) and a components breakdown. Computed by a daily batch job over the participant set, stored in wallet_score, served read-only. docs/wallet-score-spec.md, migration 0009_wallet_score, scripts/wallet-score.sh.
  • Population: any wallet in application.applicant ∪ ticketFundingEvent.funder ∪ mintEvent.minter (per chain). Refresh policy: full rescan+rescore if active in last 30 days or no row; rescore-only (no OpenSea/Etherscan calls) if dormant but score >7 days old; else skip.
  • Formula: 5 metric groups (A Holding behavior 40pts + B flip/dump penalties, C wallet age/tx/gas, D breadth/tenure/follow-through) → normalized weighted sum over enabled metrics (round(100·raw/earnable), so disabled metrics drop from numerator and denominator → mechanical phased rollout) → sybil + cluster caps → additive identity bonus → band. The hardest-to-fake metric is A2 capital lockup (ETH-days = price × time). docs/wallet-score-spec.md.
  • Surfaced as wallet score cards on the web UI (with X/Twitter link-preview fixes + cache-busting so cards don’t serve stale scores). apps/web/src/routes/{index,scan}.tsx, apps/web/src/styles.css.

Admin-managed agent collections (SHIPPED 2026-07-10, replaces runtime snapshot discovery)

  • New admin-only collection registry (admin.agent-collections route + migration 0010_agent_collection) that curates which collections are treated as agent sources, replacing the fragile runtime agent_snapshot discovery cache. Forward migration 0011 clears the obsolete snapshot payload but keeps the empty table until safe to drop. apps/web/src/routes/admin.agent-collections.tsx.
  • Why the switch (FMEA in docs/agent-snapshot-analysis.md): the daily snapshot job could silently never be scheduled, a wrong-chain RPC (e.g. Anvil on chain-id 1) produced a “healthy” empty snapshot that erased agents, and degraded refreshes still exited 0 — so monitoring couldn’t tell “refreshed” from “wrote nothing.” Admin curation removes the runtime-discovery failure surface.
  • This supersedes the 2026-07-08 WIP discovery-cloud work (GUARANTEED_MINIMUMS landing top-up + PINNED_ENTRIES for cc0mon/freaksone): pinning/guaranteeing collections in the discovery pipeline is moot now that collections are admin-managed rows.

Wallet scanning industrialized — resumable, credit-metered jobs (SHIPPED 2026-07-11)

The daily-batch wallet score (above) grew a durable, resumable, pay-per-scan job engine — the on-demand scan is no longer a single synchronous pass but a tracked job that can be paused, resumed, retried, and billed.

  • Resumable scan jobs (22455b8): a state machine awaiting_credit → queued → running → complete with checkpoints, a background scanWorker.ts (retry + continuation semantics) and a maintenance.ts driver for scheduled reconciliation + job recovery (run out-of-process via a systemd timer, deploy/systemd/aw-env-maintenance.{service,timer}.in). New domain/wallet/scanJobs.ts; new web routes scan/jobs/:jobId (progress) + a profile route; migrations 0015_resumable_scan_jobs, 0016_scan_reconciliation_safety, 0017_wallet_evidence_repair.
  • Scan credit accounting (22455b8): reserve credits upfront, charge on completion, refund overages — tables scan_credit_account, scan_job, scan_checkpoint, scan_credit_ledger; web gets a credit top-up + job-progress UI. ADRs: resumable-scan-pricing.md, scheduled-maintenance-external-driver.md.
  • Quantity-aware wallet evidence (03ea553): new walletEvidence.ts + walletScanReport.ts capture how much of each holding a wallet has (not just presence), feeding the score; migration 0014_wallet_evidence. Related fix: handle empty Alchemy ERC-1155 metadata without erroring (7114ee3).
  • Incremental, Ponder-aware analytics (e18d309): walletScan.ts/walletScore.ts reworked to scan incrementally against the Ponder read model with complete-score-snapshots instead of full rescans each time (ADRs incremental-wallet-source-cache.md, complete-wallet-score-snapshots.md).
  • Chain-authoritative holding corrections (1ba4614, 07-11) + scan safety (964934a, 07-12): the scan now reconciles stale provider holdings against the chain without inventing history — an ADR + code-rule (chain-authoritative-holding-corrections-rca.md, reconcile-holdings-without-inventing-history.md) govern how a correction is recorded. Plus OpenSea rate-limit resilience (e342dcd, 07-12): scan retries/backoff in walletScan.ts + scanWorker.ts when OpenSea throttles.

X (Twitter) identity linking (SHIPPED 2026-07-10, 8de35df)

The wallet-score identity bonus now has a concrete linking path: users link an X/Twitter account (Better Auth), gated by DB constraints so one X account maps to one identity. New XAccountLink web component, backend authConfig.ts, migration 0013_twitter_account_constraints. ADR .fdb/adrs/x-identity-linking.md. apps/backend/src/{auth,authConfig}.ts, apps/web/src/components/XAccountLink.tsx.

Layered agent-discovery caching (SHIPPED 2026-07-10, 500ed1c)

Now that agent collections are admin-managed (above), discovery reads are cached in layers instead of re-fetched: OpenSea metadata cached in memory + Postgres (agentMetadataCache.ts, migration 0012_agent_nft_metadata_cache), discovery fan-out bounded, and backend/edge/browser freshness aligned; a Postgres advisory lock (postgresAdvisoryLock.ts, bounded connections) coordinates refreshes so post-lock refreshes are preserved and concurrent refreshes don’t stampede. ADR agent-discovery-layered-cache.md + refresh-coordination RCA. apps/backend/src/domain/agents/{agentDiscovery,agentMetadataCache}.ts.

Onboarding journey — chromeless 3-stage welcome + share-gated breakdown (SHIPPED 2026-07-13 → stabilized 07-15)

A first-run onboarding overhaul that turns the landing page into a guided, gated funnel into the wallet score. Landing “Notis On” is now the Connect-wallet gate; a first-run wallet flows through a new chromeless /welcome state machine into the profile. docs/onboarding/ design docs; .fdb/domain-language/onboarding-journey.md.

  • Three-stage journey (one three-node rail, STEP n OF 3 label): (1) Wallet Authentication — connect, keep wizard open, then a separate Sign in with Ethereum action; (2) link X to the resulting Better Auth user in the same wizard; (3) one compact reputation surface stays mounted while the complimentary evaluation moves from pending-scan to the wallet’s real score (dial, facet bars, reasons, agent holdings). Identity/X loading states advance monotonically and never fall back to the standalone Connect entry; complimentary score work starts only after both gates pass (SIWE session matches wallet and an X account is linked). Closing the wizard before stage three returns to the landing entry and never grants protected access. Stabilized across 052b2ad/fb3c231/a9f1ba6 (07-15: onboarding+protected-app composition, reputation-transition stability, rep-card layout).
  • Share-gated full breakdown (6eb821e, migration 0018_share_unlock): the profile shows a score module leading a desktop 2:1 layout; sharing the score card on X unlocks the full signal breakdown, verified via the X API on the linked account — the unlock is stamped on the score record itself (share_verified_at + share_post_id on wallet_score) so it’s portable across sessions. Scan report gets an accent score tile + shared holdings strip; the 80px grid texture was removed. Local QA: seedDevScore (pnpm qa:seed-score) + DEV-only X-skip / breakdown-unlock affordances gated on LOCAL_DEV_CHAIN.
  • Scan routes now protected behind SIWE + X (0ac9a72, 07-14) via an auth-guard; nav simplified to Home / Scan / Profile only for now (e2707c4, 07-12).

Agent minting path — agent-wallet-native first (decision, 2026-07-12, e4298e5)

A decision record (docs/agent-minting-path.md + a 516-line docs/agent-minting-all.md) for the goal “users’ agents mint from whitelists and public mints autonomously.” Decision: ship agent-wallet-native minting first — the agent wallet is already the applicant, the winner, and the exported address, so put the agent wallet on the OpenSea allowlist and let it mint with itself as minter. Because minter == msg.sender, this sidesteps SeaDrop’s payer-not-allowed rule entirely: no new contracts, no creator payer authorization, no delegation machinery. The research doc’s PayerExecutor architecture (human EOA on the allowlist, agent pays, NFT → human) solves a different case and is a possible phase 2, not the MVP. First target agents: the homepage collections (DePunks, freaksone, nomic, …). Ties into opensea-erc8217 / depunks agent identity.

Joyful UI skin — light-only consumer design system (SHIPPED 2026-07-16, 2538303 “new ui”)

A ground-up visual reskin of the authenticated consumer app (welcome / scan / profile), introduced as a self-contained design system rather than a token tweak. New apps/web/src/a4f.css (~1594 lines), header comment: “agents4.fun joyful design system (light-only). Source of truth: the agents4fun-desktop / joyful / report HTML mocks.”

  • Scoped isolation via .a4f-shell: everything is namespaced under .a4f-shell, which wraps the _protected layout (_protected.tsx), so legacy surfaces (sweeps, whitelists, admin) keep the old .protocol-shell skin untouched. The joyful tokens also remap the shared protocol tokens (--bg-page, --accent, --text-primary, --ring, …) inside the shell so shared inputs/buttons/chrome blend into the new skin without per-component rewrites.
  • Palette + type: pink-forward — --pink #e04a92 (+ darker --pink-dark/-deep/-text variants darkened for WCAG AA, brighter pink kept only for decorative stripes/borders/glyphs) — with a green accent (--green #2f7a33), warm ink #1c1b1a, soft page bg #f6f5f2, white cards. Fonts: Baloo 2 (display) + Nunito Sans (body), loaded from the root shell (index.tsx) with a comment that a wedged wallet connector “must not blank the shell.”
  • Surfaces reworked in the same commit: _protected.scan.tsx (large rewrite, +~1391), _protected.profile.tsx (~585 lines), ScoreBreakdown.tsx, ScanJobProgress.tsx, ScanCreditTopup.tsx, Header.tsx, scan_.jobs.$jobId.tsx, plus a new apps/web/src/lib/wallet-profile.ts helper and a small scanJobs.ts backend touch. This lands the wallet-score / scan / onboarding features (above) in the finished joyful visual language.

Version-2 scan blocks + incremental authorization (SHIPPED 2026-07-17, e12d302/6b5ffbf/9e4e269)

Reworks how paid scans are metered — supersedes the earlier resumable-scan-pricing model. A paid scan action immediately reserves one version-2 scan block (base price held at 0.0001 ETH, SCAN_PRICE_FACTOR=1.0) and durably authorizes a continuation batch of up to 5 more blocks, each reserved one-at-a-time only when the job actually needs it. Version 2 keeps the base price but multiplies every allowance dimension ×10 — one block now covers 100 inbound + 100 outbound Alchemy pages, 100 Etherscan pages, 500 metadata units, and 5,000 holding-reconciliation positions. Rationale (ADR incremental-scan-authorization.md): the 93-wallet test run charged 226 v1 blocks across 14 authorization batches; under v2 the same persisted work is 98 blocks, only one wallet needs >1 block (six). Reserving all six upfront was rejected (locks credit most jobs never spend); one-approval-per-block was rejected (UI latency dominated the large-wallet run). The customer UI says “scan block,” never batch, and shows immediate reservation separately from max authorized exposure (measured cohort: 0.0093 ETH reserved now / 0.0558 ETH max exposure / 0.0098 ETH expected use). Same rule covers single-wallet scans, report refreshes, and Wallet List actions; a complimentary own-wallet scan stays exactly one free v2 block and paid continuation needs a separate explicit action. Jobs pause on insufficient credit and auto-resume after top-up; competing jobs take newly available credit in durable queue order. Throughput pass (6b5ffbf) added scanProviderBudget.ts + reworked scanState.ts/walletScan.ts; actionable credit errors (9e4e269, errorMap.ts) surface a clear “add credit” message instead of a raw failure. Deploy is version-gated: v1 test data is deleted first and a deploy must fail if an active v1 job exists (no mixed-version execution). Migration 0024_incremental_scan_authorization; new domain-language scan-block / standard-scan-allowance / scan-credit-reservation / continuation-batch. apps/backend/src/domain/wallet/{scanJobs,scanState,walletScan,scanProviderBudget,walletList}.ts.

Admin observability + manual ledger credits (SHIPPED 2026-07-17, ff87665/afbccc1)

A first admin surface for support/billing. User observability (ADR admin-user-observability.md) adds an RPC set (domain/admin/users.ts, rpc/admin.ts + admin in/out schemas) exposing per-user detail; manual ledger credits (afbccc1) let an admin grant/adjust scan credit with an audited actor (migration 0023_admin_credit_actor, adminCredit.integration.test.ts). Admin routes are now protected behind the shared auth shell/guard (90c4b4e, auth-guard.ts) and the admin user-detail route was fixed (3db5008, admin.users_.$userId.tsx).

Deployment + contract hardening (2026-07-16→17)

An ops/safety batch. Deploy config: strict layered deployment configuration + test deployment is mainnet read-only (ADRs, 8250e24); the Ponder indexer isolated to its own database with bootstrap-deploy support and a disabled-sweep skip (d8dfa67/2a4011f); local DB backups now stored as plaintext (dropped the encryption wrapper, 243b132). Contracts: an owner-recovery pass adding owner-only VRF-timeout recovery and pull-based whitelist fee claims (ADRs owner-only-vrf-timeout-recovery.md, whitelist-fees-use-pull-claims.md, 5060f0a).

Scan Wallet List on the joyful skin + mascot favicon (2026-07-17, a5cfd36)

The /scan Wallet List panel was rebuilt in the a4f design system (statgrid / ctable / pills / cards) to match the rest of the page, the reputation-reasons list was dropped from the onboarding report, muted text nudged #9a9a9a → #666, and a favicon (32/256) + apple-touch-icon from the “A” mascot mark were added.

Scan workspace “control room” — nested routes + run monitoring (SHIPPED 2026-07-18, eff5225ee4962f)

A ground-up rebuild of the /scan surface into a multi-route control room, plus a durable way to watch what a scan is doing per wallet.

  • Nested scan routes through an outlet: /scan now splits into /scan/list (Wallet List), /scan/runs (run index), and /scan/runs/$actionId (run detail), rendered via a new ScanWorkspace.tsx and an <Outlet /> in _protected.scan.tsx — the parent renders “Scan One” only for the exact /scan path and delegates nested URLs to the outlet. RCA protected-nested-route-outlet-rca.md records the bug it fixes (parent matched the child URL but showed only the parent page because it had no outlet); a code-rule now requires an outlet path + regression test for any route nested below a route file (6e09121). Header, action controls, and credit/wallet links were streamlined in the same pass (14e79e7/fee2613).
  • Wallet List Actions monitor jobs through a link table (ADR wallet-list-action-job-monitoring-link): a new owner-scoped many-to-many wallet_list_action_job(action_id, job_id, entry_id) link, populated in the same tx that authorizes or reuses scan jobs (read-only metadata — it does not change execution, billing, retries, or job ownership). Per-entry outcome rows (wallet_list_action_entry) record eligible-vs-skipped + the exact skip reason, so the console can page skipped wallets without treating them as jobs. The Runs index lists authorized Wallet List Actions first, then standalone active/recoverable jobs; completed reports stay in Profile; the preview’s reserved-now amount stays distinct from max exposure. Migrations 0025_wallet_list_action_jobs, 0026_wallet_list_action_skip_reasons, 0027_wallet_list_action_entries. UI: grouped run progress on the Runs page, nested run consoles, a grouped status rail, and cancelled outcomes kept visually distinct (8771066/f2b6d8e/2968508/ee4962f).

Shared OpenSea collection presentation cache (SHIPPED 2026-07-18, 551927a)

Collection art (logos/token images shown in reports) moved out of per-user frozen report metadata into one system-wide typed cache keyed by (chain, collection contract), resolved at report read time. ADR shared-collection-presentation-cache.

  • Why: the scan worker treated an unsupported Alchemy metadata call as a successful null, persisted image-less metadata as “fetched,” and froze those nulls into immutable reports that could never improve. Measured: test holds 11,406 historical evidence contracts but only 32 verified collections are actually visible in reports — a full historical crawl is mostly wasted provider traffic.
  • Behavior: prefer OpenSea collection art → fall back to the first valid OpenSea token image → deterministic initials/contract placeholder if neither. Cache is shared by every user, keeps positive metadata 24h, retries negatives after 15 min, and serves stale images through provider failures. The stored report version stays immutable while its read model overlays current cached presentation; existing report collections are repaired first by low-priority maintenance, new misses recorded for the same path — never blocking report reads or scan completion. Presentation refresh never touches evidence, verification, scoring, scan status, billing, or entitlement. Migration 0028_collection_presentation_cache; new collectionPresentationCache.ts pulls ~123 lines of art-fetching out of scanWorker.ts.

Consolidated scan / credits / X-identity profile + repositioned copy (2026-07-18, 6bde458/0613c8f)

  • Profile consolidation (6bde458): a large a4f.css rework (+~1125) unifies the scan workspace, credit top-up, and X-identity surfaces. ScanCreditTopup.tsxcredit.tsx, a new ProfileMasthead.tsx + ActivityFeed, and an expanded ScanWorkspace.tsx; auth/authConfig touched so the profile reflects the linked X account.
  • Public copy repositioned around reputation (0613c8f): the landing headline is now “AI Agents Finally Have a Reputation” and the pitch is “Connect a wallet. Get a verifiable trust score. Let agents compete for allowlist spots on equal footing with humans.” — the messaging pivots from the old “putting agents to work / scattered for fun” framing to the wallet-score / equal-footing story across index, agents, create, docs, __root. The meta title now literally uses the string agents4.fun (resolves the prior open question below).

Mainnet-fork local QA (2026-07-18→19, b8dd498/26c6ece)

scripts/local-qa/ gained an Anvil mainnet fork so wallet scans complete end-to-end locally (real chain data behind the fork): a refresh-wallet-scores.ts backend script, an eth-price-refresher.sh, and updated anvil.sh/deploy.sh/up.sh/START-SERVER.md. Follow-up 26c6ece keeps the fork pinned behind the real chain so local QA doesn’t drift ahead of mainnet state.

Multi-wallet account linking — in-session switching + advisory OpenSea suggestion (SHIPPED 2026-07-19, 9ca352d)

One Better Auth user can now attach multiple EVM wallets and switch between them without re-signing each time. New apps/backend/src/domain/wallet/linkedWallets.ts (305 lines) + apps/web/src/components/LinkedWallets.tsx (321 lines); migration 0029_multi_wallet_session adds a session.activeWalletAddress column and a case-insensitive unique index on walletAddress (lower(address), chainId).

  • SIWE only when it adds authority (ADR linked-wallet-switching-and-unlinking.md): a wallet requires fresh SIWE the first time it’s linked and on normal login after session expiry, but switching to an already-linked wallet inside a valid session needs no new signature — the current session already controls every wallet attached to that user (on-chain actions still require the selected wallet to submit its own tx). The connector must expose the selected linked address before the switch completes, and the session records it as the active wallet.
  • Wallets are private, no “primary” (same ADR): the product has active + linked wallets but no user-facing primary/root wallet, and it never reveals that two addresses belong to the same user. Any library-internal primary flag has no product meaning. A linked wallet can be unlinked from its row overflow menu, except when it’s the active wallet or the user’s last linked wallet. Unlinking removes future login authority for that address but does not delete or transfer credits, jobs, report entitlements, or score/scan history (those stay attached to the address).
  • OpenSea discovery is advisory only (ADR opensea-wallet-discovery-is-advisory.md + domain-language wallet-link-suggestion): OpenSea’s public account response returns only one address (not an enumerable list) and hidden wallet relationships are intentionally private, so a supported OpenSea profile can surface one optional wallet-link suggestion — an untrusted candidate that only becomes a linked wallet after the user connects it and completes fresh SIWE. Linking is EVM-only (Solana/SIWS rejected as out-of-scope), stays optional (one post-onboarding prompt + persistent Profile management, no required onboarding stage), and OpenSea failure/absence never blocks sign-in. A wallet already owned by another Better Auth user fails closed — no automatic account merging. Aliases to avoid: “discovered / verified OpenSea / auto-linked wallet.”
  • Also lands in the same commit: multi-wallet SIWE plumbing (authSiwe.ts, sessionWallet.ts, rpc/router.ts +147), joyful-skin styling for the wallet rows (a4f.css +65), _protected.profile wiring, and tests (rpcSessionWallet.test.ts, wallet-auth.test.ts, _protected.test.tsx). Refines the existing SIWE/protected-app-shell/onboarding model. Merged to dev via 908137e (07-19).

Design-system consolidation — shadcn rip-out + own UI kit (WIP, 2026-07-20, a17bdca)

⚠️ In progress, not shipped — this is a single “dev checkpoint” commit on dev (a17bdca, 2026-07-20) plus an uncommitted working tree (a4f.css, ScanWorkspace, credit, scan/ui-kit/wallets routes, styles.css, a new apps/web/src/ui/dialog.tsx). Direction, not a finished feature; recorded so the next compile can track it. Commit message: “design-system consolidation, shadcn rip-out, multi-wallet scan WIP.”

  • shadcn/ui removed: the whole apps/web/src/components/ui/ shadcn set is deleted — badge, button, calendar, date-time-picker, dialog, label, popover, select, tabs — and the corresponding Radix deps dropped from apps/web/package.json. Replaced by first-party primitives that live in the joyful/protocol token systems rather than a third-party component lib: new apps/web/src/lib/{badgeTone,statusTone,uiKitAccess}.ts tone helpers, reworked StatusBadge.tsx / SweepStatusBadge.tsx, and a own apps/web/src/ui/dialog.tsx (WIP) standing in for the removed shadcn dialog.
  • New /ui-kit route (_protected.ui-kit.tsx, ~2428 lines): an in-app component showcase / kitchen-sink for the consolidated design system, access-gated via uiKitAccess.ts. The living reference for the a4f + protocol primitives.
  • New wallets surface (_protected.wallets.tsx + _protected.wallets_.$wallet.tsx, multi-wallet detail) and a new _protected.scan.$scanId.tsx; broad touches across scan routes, walletList.ts (+438), walletScore.ts, router.ts, and most existing routes as they migrate off shadcn onto the new primitives. Extends — does not replace — the agents4fun joyful .a4f-shell skin.
  • Continues the multi-wallet linking work (above) on the front end (“multi-wallet scan WIP”). Not yet merged past dev/deployed; treat as the current direction, verify state on the next run.

Wallet-centric IA — scan console + report merged into the wallet page (SHIPPED 2026-07-21→22, 354f58d/44b6e23)

The a17bdca design-system-consolidation direction (above) landed as a shipped wallet-centric information architecture: the per-wallet page is now the single surface for everything about a wallet, and the standalone scan-job route is gone.

  • IA restructure + design polish (354f58d, 07-21): Wallets promoted to a top-level nav item and Home dropped (the logo now links home); the internal Scans/Wallets tabs were removed from every surface. The Wallets list gains ENS names, linked-wallets-sorted-first ordering, only scanned rows link through, and auto-imports + auto-scans your linked wallets; the wallet page guards to scanned wallets only (with a not-scanned fallback). Backend wallet.list rows now carry ensName + isLinked (walletList.ts, router.ts). Paid reports unlock the signal breakdown, and the X-post gate now applies only to the free complimentary scan (hasPaidReport wired into profile/detail, walletProfile.ts). Scan-page polish: quote verifications moved into the TARGET WALLET card (divider, main-CTA “Review and authorize”, Close; search locks while a quote is open); CSV upload became a silver “Upload CSV” modal that creates the scan and returns info inline; the Activity feed collapsed to Active / Needs action / Done (Active default, no card box, whole-row-clickable with white hover, white segmented-control track); modals use the page cream background everywhere. 205 web tests green.
  • Scan console + report merged onto /wallets/$wallet (44b6e23, 07-22): the wallet report now lives at /wallets/$wallet — full Dashboard, share-gated breakdown, X link, scan history, back → Wallets list. A new ScanJobConsole.tsx (443 lines) was extracted (SSE progress + continue / add-credit / cancel / retry + complimentary card) and embedded on the wallet page while a scan is in flight, so one wallet URL renders every state: report → Dashboard, in-flight → console, free scan → score-only + paid upsell, else not-scanned. /scan/jobs/$jobId is now a thin redirect to /wallets/$wallet (old links still resolve), and every Console link (scan-detail rows, activity job rows, matchingJob, progress card, outcome rail, wallets-list status) was repointed there. The bulk scan detail became a routed full-width takeover (Close X, slides up on open / down on close, ease-in-out, reduced-motion aware; _protected.scan.$scanId.tsx). 209 web tests green; typecheck + biome clean. This graduates the shadcn-rip-out/multi-wallet direction into the shipped consumer IA.

Agents create whitelists via the public API + partner collections (SHIPPED 2026-07-25, 8519f91/be026ae)

The agent-facing half of the agent-x402-payments bridge: an external agent (e.g. a depunks agent) can now create a whitelist through the public /api the same permissionless way it applies — this is what depunks’ create-whitelist skill calls.

  • createWhitelist in @aw/agent-client (packages/agent-client/src/index.ts): reads the chain fees, saves metadata by externalRef, signs the wallet intent, and broadcasts from the artist wallet (the router requires msg.sender == artist, so the agent must sign — an owner-signed create would put the human on the allowlist). scripts/agent-create.mjs wraps it for local QA.
  • /api/chains now publishes standardHookAddress — a required createParams field, so without it an agent outside this repo can’t build a create tx at all (zero address → null). closeTime is now derived from chain block time, not the wall clock, so a warped Anvil doesn’t mint whitelists that close in the past. Whitelist creation also surfaced as an in-app action (be026ae), with navigation/access unified (e523e04) and lifecycle QA regressions fixed (27a2a09).
  • Partner collections: Koalified joins the curated agent collections (migration 0032_koalified_agent_collection; the other four partner contracts were already seeded in 0010), and the landing page now guarantees all five partners a minimum presence alongside DePunks.

Score holdings that are curated or bought (SHIPPED 2026-07-25, 027b11e)

Fixes a wallet-score blind spot: Group A only counted lots in the ~55-entry code registry, so a wallet holding 141 NFTs across 28 collections had only 14 units visible to the score — including 78 NORMIES, a collection the product features on its own landing page.

  • verified is now app-curated (per a new ADR): a collection counts when the code registry curates it, an active agent_collection row curates it, or OpenSea’s safelist marks it. Resolution changed from first-source-wins → any-source-saying-yes-wins (reading the stored value first made an early false permanent, so a collection could never become verified without wiping the row). Migration 0033_verify_curated_agent_collections backfills.
  • A1 accepts priced lots: a lot counts toward hold behavior when its OpenSea listing carries a price (bought/curated), not only when it’s in the registry.

Scan fully collapsed into Wallets + floating scan widget (SHIPPED 2026-07-22, f5a83e00963f71)

Completes the wallet-centric IA (07-21→22 354f58d/44b6e23): /wallets is now the only home and the standalone scan surface is gone.

  • Scan → Wallets (f5a83e0): all /scan* routes redirect (q and batch survive); /scan/list and /scan/runs* are deleted along with ScanWorkspaceNav and the ActivityFeed. The wallets page gains a scan bar with an inline CURRENT SCAN console (recovered after reload, dismissable), a unified CSV import → preview → authorize dialog, batch chips + a ?batch= inline view, per-row ladder actions, and per-row remove via a new wallet.list.remove endpoint. Backend splits scored (free report) from completed (paid), adds attention/scored list filters, and reports cost-to-complete in statistics. One status vocabulary (stateLabel) replaces the three per-surface ones; Dashboard + CSV parser move to shared modules.
  • Floating global scan mini-widget (8ed1f2d): the current-scan console leaves the page layout and becomes ScanMini — a fixed 320×124 card bottom-left (80px full-width dock on mobile) showing wallet, state pill, live stripe, one meta line. GlobalScanMini mounts in the protected shell so it follows the user across pages, derives the active single-wallet job from the jobs overview, stays after completion with a report link until dismissed, and hides on that wallet’s own page. Mouse-draggable with pointer capture, snaps to a 16px grid + screen edges, clamped to the viewport, persisted in localStorage.
  • Batch-aware widget + Phosphor icons (0963f71): the widget now covers batches (a BATCH card with determinate progress + completed count linking to the filtered Wallets list) and pages through multiple running scans with a slim header; on /wallets the batch chips became a dropdown filter. Icons are now @phosphor-icons/react project-wide (CLAUDE.md convention) — every hand-rolled inline SVG replaced. Scan widget header made permanent on both card variants (5441eb4); wallet scan access + progress flows fixed (4d4ad4e).

Migration-ledger repair + test isolation (2026-07-25, 98e27e0/dd1253e)

Infra hardening, no product surface.

  • Renumbered-ledger repair (98e27e0): databases that applied 0029/0030 before they were renumbered record names that no longer exist as files, so Kysely refuses to run anything (“corrupted migrations: … missing”). Both are pure bookkeeping (the schema is identical), so migrateToLatest now renames the ledger rows and restores their run times in name order before handing to Kysely — conditional + idempotent (never-applied, already-repaired, and fresh DBs are all left alone). It can’t be a migration file since the ledger is validated before any migration runs. Adds migration 0031_wallet_list_action_history.
  • Integration tests isolated from the developer DB (dd1253e): scanJobLease claims work by query (next due job, credited payments), so against a dev DB with 1005 parked jobs it claimed real ones and failed nondeterministically. It (and sweepPersistence) now build a scratch database per run like walletList/adminCredit already do; walletScore’s refresh-batch test now pins an empty ConfigProvider so an ambient ETHERSCAN_API_KEY can’t send it down a live-RPC path.

Onboarding/admin fixes + deploy hardening (2026-07-25, c04d02d/754c330/f7b92e5)

Three follow-on fixes to the multi-wallet linking (9ca352d) + deploy work; no new product surface.

  • Acting/admin wallet now honors the selected linked wallet (c04d02d): actingWalletFromSession and isAdminWalletSession previously resolved a user’s wallet from the SIWE email only, so after switching to a linked wallet the acting wallet and admin check still keyed off the login wallet. Both now prefer session.activeWalletAddress (when a valid address) over the email-derived SIWE wallet, matching backend authorization — but still require a valid SIWE email as the floor (actingWalletFromSession returns null without one). hasAdminSession now passes the whole session (not just email) to isAdminWalletSession; auth/config errors still fail closed (no accidental admin grant). Plus a landing-overlay CSS fix so buttons — not just links — stay clickable during onboarding (.landing-overlay button { pointer-events: auto }).
  • Keep better-auth external in the SSR build (754c330): Vite was inlining some better-auth submodules into the SSR auth chunk while leaving better-auth itself external; the inlined code’s own imports (jose, @noble/*) then leaked out as bare specifiers that pnpm never links into apps/web/node_modules (they’re transitive), so the deployed server died on startup with ERR_MODULE_NOT_FOUND for jose. Externalizing the whole better-auth package lets Node resolve it and its deps from better-auth’s own node_modules. apps/web/vite.config.ts.
  • Deploy runbook documented (f7b92e5): CLAUDE.md now records the deploy.sh entry point, what it needs locally, the order of its steps, and the health gates that trigger a rollback.

Wallet Score formula overhaul v6→v8 (SHIPPED 2026-07-26 → 27, ff079ba353c71232267d5)

The Wallet Score was rebuilt from the ground up across three formula versions in ~two days. The currently shipped version is v8 (WALLET_SCORE_FORMULA_VERSION = '8' in walletScore.ts); v6 and v7 were intermediate landings that ship in the same window. Two orthogonal changes: (1) a collector-reputation reframe of what the score means, and (2) a fixed-base rewrite of how it’s computed — replacing the old normalized-denominator + identity-bonus mechanics entirely.

What the score means now (v6 reframe, cc70cb5): the score measures collector and participant reputation, not creator/artistic merit. Mint acquisition is neither rewarded nor penalized (the old B2 mint-and-dump metric and its mint-and-dump Sybil input are removed); later disposal is just ordinary collector behavior. ENS no longer earns direct core points — it’s only an identity-confidence input to the Sybil classifier. Human Passport scoring is removed entirely (the published formula must describe executable behavior; Passport was an unreachable bonus). Public UI keeps the name “Wallet Score”; the internal domain term is now collector-reputation-score (.fdb/domain-language/collector-reputation-score.md).

How it’s computed (v7 fixed base 353c712, then v8 penalty cap — currently shipped):

  • Positive base = a fixed 100 points (no normalized denominator): three 30-pt NFT signals + 5 wallet-age + 5 tx-history. Network fees (old C3) removed.
    • A1 Long-term holding credit = 30 × avg(min(open-lot age / 90d, 1)) × min(scoreable open-lot count, 3) / 3 over priced-or-curated acquisition lots. Continuous, not a binary 90-day threshold (an 89-day hold now earns credit). Counts lots, not NFT units — one ERC-1155 acquisition = one holding decision regardless of quantity; full confidence at 3 lots. A transferred-in lot’s clock resets to the receiving-wallet timestamp (no inherited hold age) and is A1-eligible only when curated (it has no inherited acquisition cost).
    • A2 Known-cost ETH-days = 30 × clamp(log10(ETH-days + 1) / log10(1001)) — the hardest-to-fake capital-lockup signal, coefficient doubled from v6. Open + closed known-cost FIFO lot portions each contribute cost × min(hold duration, 365d); closed capital decays linearly to zero over the 365 days after that disposal’s own timestamp. Unknown-cost portions contribute zero without discarding known-cost portions from the same disposal. Advisory current market value never fills a missing acquisition cost.
    • A3 Quality holdings (v6 values doubled): first held blue-chip collection 20 pts, each additional +5 (cap 30); a wallet with held verified collections but no blue chip earns 12. Koalified/NORMIES stay A1-scoreable when verified/curated but don’t become blue-chip.
  • Quick-flip penalty (v8 caps it at 10, down from v7’s 15): 10 × quick-sale vote share. A quick sale = a confirmed sale before day 14 with a reliable acquisition timestamp. Each sale event gets one vote per FIFO acquisition-lot portion it consumes, regardless of unit quantity; separate sales consuming the same lot are separate decisions. No eligible sale evidence = neutral (no reward, no penalty — this is the change that finally makes no-sale wallets explainable in the breakdown). Transfers and burns never enter the penalty.
  • Pre-cap score = max(0, positive − penalty); Sybil + funding-cluster caps remain hard ceilings applied afterward; public score rounded to an integer. Breakdowns now expose Positive evidence x/100, Quick-flip penalty −x/10, the pre-cap result, and any applied cap — no normalized-base row.

Evidence / reconciliation rules that make the rewrite safe:

  • NFT burns are score-inert (ff079ba, .fdb/code-rules/nft-burns-are-score-inert.md): only 0x0 and 0x…dead count as burn sinks; consume the FIFO units + increment burned_units, but add no closed units, holding time, sale evidence, proceeds, cost basis, or realized P&L. Other outbound recipients stay transfers/sales.
  • Reliable evidence subset with exclusions (38500f7): a complete finalized transfer replay may publish a score even when isolated contract/token positions are excluded — the scan persists every excluded contract:tokenId key, the score replay removes those positions and all their holdings/economics, and the report lists them as degraded evidence. Missing/malformed/count-mismatched exclusion keys stay incomplete → cannot publish. Complete-but-insufficient scans are still blocked from scoring (e3e3a39, d419088).
  • Formula-only rescoring replays complete cached canonical transfers daily (incl. dormant wallets, applying closed-capital decay) with no provider calls; wallets without sufficient evidence wait for normal refresh. Mainnet-only until another chain has equivalent complete canonical transfer evidence. Each version invalidates the prior version’s snapshots; migration 0036 (v7) then 0037 (v8) clear wallet_score ONLY on https://test.agents4.fun (other environments keep serving until re-scored).
  • Advisory wallet portfolio estimate (1f37008, market-values-and-trending ADR): an Ethereum-only USD snapshot beside ETH balance (OpenSea-priced NFTs + Alchemy-priced ERC-20s) — presentation only, never touches score / P&L / scan evidence / completion / billing / report immutability. Collection rows show observed-hold-duration like 73d observed (45d sold) / 73d observed (no sales).
  • Open question (grill todo): v7/v8 regression-wallet fixtures for 0x7eee…dbfd and 0xdffc…52c88 still need capturing — v6 screenshots/protected reports don’t expose the open-lot ages + per-sale FIFO evidence v8 needs.

Brand mark + UI polish pass (2026-07-26 → 27, 48cb3ae32267d5)

The joyful consumer skin got its first real brand identity and a round of component polish.

  • A4F brand mark + generated asset set (48cb3ae) then derived from the logo instead of redrawn (4d01cd9): new favicons (16/32/svg/apple-touch) and apps/web/src/assets/brand/a4f-*.svg (mark / four / tile / lockup variants, ink + paper). Assets come out of a Python brand generator whose bytecode is now gitignored (4aed113).
  • Account gets a menu; authorize modal gets a footer (a655357): new first-party apps/web/src/ui/menu.tsx (132 lines); the Header account control becomes a dropdown menu and the scan authorize modal gains a footer.
  • Profile identity moved into a left rail (fadac10): ProfileMasthead + _protected.profile reworked, the profile route trimmed ~144 lines as layout moved into the component.
  • Progress split into a fraction bar + an activity stripe (b571300): new apps/web/src/ui/progress.tsx + lib/batch-progress.ts — a determinate fraction bar (how far) plus an indeterminate activity stripe (still moving), adopted by ScanMini / ScanWorkspace / ScanJobProgress.
  • Native selects → shared Select, header unwrapped (32267d5, 07-27): a large a4f.css pass (367 lines), a new ScoreRing component, and reworked ProfileMasthead / LinkedWallets / wl.create; native <select>s replaced with the first-party Select primitive.

Public wallet profiles + phone-fit UX pass + agent-holder bonus + sale-price scoring (SHIPPED 2026-07-27 → 28, 80b5297c4bc013)

A dense two-day push that turned /wallets/$wallet into a public profile, made the whole authenticated app fit a phone, added a pay-nothing scan-credit bonus for agent holders, and closed a scoring gap where prices landing after a scan never moved the number. Branch dev, HEAD c4bc013 (2026-07-28 00:42).

  • A wallet page is now a public profile (80b5297): /wallets/$wallet adopts the profile template — a 360px identity rail (score ring + identity + a single act: Rescan, where /profile has Full report) holds sticky beside the scrolling data column and releases when the grid ends. New address-identity/avatar system (walletIdentity.ts): four avatar sources, best evidence first — the X picture (session-only, so only ever your own), the ENS avatar, the OpenSea PFP (api.opensea.io/api/v2/accounts, cached in memory 6h), and failing all three a generated AddressGlyph — an xorshift seeded by the address draws six points as a ridge chart over the wallet’s own gradient (a chart, because that’s what the site is about; seeding avoids 0x1111… rendering flat). The glyph replaces the two-character disc everywhere the ring appears, /profile included. Score parts and the full signal breakdown are owner-only (how a stranger’s score was built is their business); holdings, values and collections stay public. Six figures share one 3×2 plate; the scan-history list is gone (last-scan timestamp lives on the rail, run count survives as a tile); the evidence pill and back link are gone (a profile, not a report with an exit). The live scan card keeps its own accounting (charged blocks + Cancel moved inside it) and drops its 720px cap.
  • Fit the whole app to a phone (9f7aa19 profile, a92512c list, 9b74a12/7497bfc onboarding result, bd74f26/b89c429/8002771/1043286/d55e870 landing+steps, 8b5b8d8 authorize modal, 8ee219c wallets hero): nav items take equal shares with centred labels; the identity rail stops being sticky below 980px (a same-weight later rule had silently undone the reset — media queries add no specificity, so the reset now comes last); the collection table collapses to Collection + Held with no sideways scroll and its tools fold to one line (scope + filter dropdown + a search button that opens as a bottom sheet); the figures plate goes 2-across on a phone; the authorize modal’s wei-as-ether grid drops to one column so content stops being clipped by the scroll container. Onboarding’s third step now fits above the fold (mark + step-count become one top row, the stage rail becomes three dashes) and the result screen was condensed twice (546–617px content at 390px). Copy was un-duplicated across the six connect-step slots (b89c429), and the X mark replaces the letter “X” in headings/buttons while the word stays for screen readers (1043286).
  • One modal style, one footer (ca67b34): six modals had drifted (rendering on card-white vs page-cream, portaled out of .a4f-shell so token-scoped hairlines drew undefined and vanished, inconsistent footers). DialogFooter is now one row at every width with a rule flush to both panel edges; the last child fills the row (single button = full-width, a pair reads as ghost-retreat beside the act). Scan-confirm / wallets / search / credit / CSV / onboarding modals all conformed; the credit modal’s action moved into the footer.
  • Agent holders earn scan credit (9b74a12): a wallet holding an agent NFT earns 0.01 ETH of scan credit, once per account for all time, claimed by sharing the score card. Keyed on the account, not the wallet (one agent walked between ten linked wallets would otherwise pay ten times); verified through the same share-verify path the breakdown gate uses; the grant re-reads the holding count server-side inside the account lock because it moves money. New agentBonus.ts, shareCard.ts, shareCardRoute.ts, ShareCardDialog.tsx. Cards are drawn by satori into SVG with glyphs as paths (keeps fonts off the server, browser rasterises on save); two layouts (with/without an agent-holdings strip). X web-intent carries text only, so the flow is “save, then compose”, not a shortcut.
  • Why no card had ever rendered on the deployed site (a79f44f): the prod server served only /assets/ (hashed bundles) and 404’d everything else — so favicons, the logo and the two fonts never served, and the share-card renderer, which fetches its fonts over HTTP, died on the first 404. It now serves any request naming a real file under dist/client (routes aren’t files, so nothing is shadowed); only /assets/ keeps the immutable cache header. Paired with 05b2a04 (declare escape-html — satori’s external CJS dep landed where apps/web couldn’t see it under strict pnpm, killing the built server on first import; version already in the lockfile so no new package) and 84f78f9 (the web health gate now retries so a slow cold SSR render no longer rolls a sound release back).
  • Share the card, not a bare link (e18056e): the X button on the profile and wallet rails now opens the card dialog (where the card is saved and the post verified) instead of a text+link composer — X lets only the posting app attach an image. One ShareScoreButton owns the dialog state so the three rails can’t each keep their own; the locked-breakdown share keeps its own unlock gate.
  • Re-score when sale prices land (e91195e): a scan publishes report + score at once, then queues price enrichment — but cost basis carries most of the holdings marks (A2 outright, A1 via its priced-lot gate), so a wallet scored before its prices exist reads low, and enrichment used to re-publish priced evidence while keeping the old score (one test wallet sat at 43 for a day, came back 73 on the next scan — same formula, same block). Enrichment now recomputes and publishes the score, and a one-off sweep (rescore-priced-wallets.ts) fixes wallets scored before this existed. While prices are in flight the on-screen score is labelled a floor. Then a24dd18: prices also identify disposals as sales, which feed the quick-flip penalty, so a pending score can fall as well as rise — the backfill moved 64 wallets, 58 up / 6 down (one 45→36), and the marker/copy now say “can still change”, not “may rise”.
  • Show the breakdown on any wallet you paid to scan (c4bc013): the backend already unlocked the breakdown for a paid report (the X-share gate is the price of the free scan only), but the page still hid it behind ownership — so a user who paid to scan someone else’s wallet got a score with no workings. The breakdown and the rail’s signal bars now follow the report (a profile only reaches this page for your own wallet or one you paid for); the locked card still asks for the share, and only on your own wallet.
  • List liveness + delete safety (cb3bb29/8b09091): the floating scan mini-widget was the only thing telling the app a scan had finished, and it unmounts as it leaves the overview — sometimes beating its own poll, leaving the page stuck on “Scanning” until reload; the wallets overview now reports the finish itself and re-reads while work is in flight (bounded, so a score that never publishes can’t poll forever). Removing a wallet from the list now opens a confirm dialog naming the wallet (short name over full address); the report is not deleted with the entry, and the copy says so.
  • Smaller polish: bba6b79formatEthCapped trims 18-decimal balances to seven decimals off the exact formatEther string (no float touches the wei), says <0.0000001 rather than round dust to zero; adopted by scan preview, credit balances and ledger rows. edcff28 — Anvil knows nothing about a mainnet contract, so local QA seeds inline SVG faces for the three mock agents and START-SERVER.md documents re-running onboarding from scratch (only account #1’s binding ownerOf read lands, so it’s the only preset that shows an agent).

Selection-control primitives + unified agent entrypoint + first CI (SHIPPED 2026-07-28, 52022af59993f6)

A same-day batch (branch dev, HEAD 59993f6, 20 commits past c4bc013) that shipped the previously-uncommitted form primitives, unified the agent-facing API/MCP surface behind a single canonical door, formalised the agent-holder bonus and X-share proof as decisions, redesigned the landing gate, cut the wallet list to four states, and — for the first time — gave the repo a CI.

  • Selection controls are now real fieldsets (bab4ca2, was the 07-28 WIP): a dropdown for a handful of answers and a checkbox for two both hide the answers until asked. Seven primitives put the choices on the page — tile-group, chip-group, segmented (a ladder), choice-cards (rich), stepper, toggle-row, and the field wrapper that labels them — rendering as real <fieldset>s so the grouping is semantic, not drawn. The whitelist create (_protected.wl.create.tsx +650) and detail (wl.$chainId.$whitelistId.tsx +204) pages are rebuilt on them, the UI Kit (+285) carries the set so the next form starts here, and a new theme.css (+333) holds the token layer.
  • /agent.md is the single canonical agent entrypoint (c4c10a5 + ADR canonical-agent-entrypoint, new LLM.md): the service exposed several agent docs and machine contracts but no authoritative first read. Now /agent.md owns authority/trust/workflow/version/recovery and links every generated contract (manifest, OpenAPI, role-scoped MCP, signing procedure, MCP config); /llms.txt drops to a minimal pointer to it, and public agent/API/MCP responses advertise it via Link headers. The agent surface was unified and de-duplicated: new api/agentActions.ts (+268) + api/agentManifest.ts (+101) + api/agentDiscovery.ts replace the deleted domain/manifestActions.ts (−217), domain/agentManifest.ts (−63) and usecases/getAgentManifest.ts. Role-scoped hosted MCP is now explicit — /api/mcp/user (applicant-wallet actions) vs /api/mcp/artist (artist/whitelist-owner actions) — over a clean /api public agent HTTP API vs /rpc private browser API split (/rpc is intentionally absent from OpenAPI + manifest). Backend stays non-custodial: protected actions use a caller-signed EIP-712 Wallet Intent and return unsigned tx data for the caller to broadcast. Directly refines agent-x402-payments.
  • Agent-holder bonus + X-share proof are now decisions (6cb4201 + ADRs agent-holding-bonus, share-gated-score-breakdown): the 0.01 ETH agent-holder bonus (shipped 9b74a12) is formalised as one claim per Better Auth account, ever — eligibility re-read from Ponder-backed ownership at claim time, made idempotent by an account-keyed unique ledger entry + account-row lock (rejected once-per-wallet → linked wallets multiply; once-per-token → turns it into a redemption registry; trusting the browser count → moves credit on client data). Score-breakdown X-unlock now requires a pasted HTTPS X post URL whose post is read through the X API and accepted only when the stable author ID matches the linked X accountId and the post contains/links agents4.fun — replacing the ambiguous “search recent posts”. X linking now also requests tweet.read + offline.access (used only to refresh auth and look up the submitted post; X is never a sign-in or write authority).
  • The repo finally has a CI, and it caught a live 500 (53e6390): burns arrived after the report format’s last version bump, so reports written between are stamped v3 with no burnedUnits — 341 of 811 on test (42%). Reading one through the strict report schema threw, taking the wallet page down with a 500 where a report used to render; the reader now fills the field with '0' (a scan before burns were counted found none) and both display-only surfaces treat an undecodable report as absent rather than crashing (the strict decode stays for callers that must not guess). The reason it was never caught: nine test files skip themselves when no database answers, and there was no CI at all. There is now — .github/workflows/ci.yml (+84) runs with a Postgres database plus a guard that fails the run if the integration tests silently skipped.
  • Landing gate leads with its promise + names the collections (b988de1): the gate gave the credit line and the promise the same 17px class so nothing led. The promise now takes a 44px display line (one word in brand pink), “Powered by DePunks” drops to the credit it is, the grey paragraph becomes three beats in real order (connect → score → spots), and the button names the reward. A strip under it names the collections already here and counts them — the count now asks Ponder for a real totalCount (computed only when selected) instead of the sampled-cloud cap of 40; collections with no agents are dropped rather than padded, hovering a bubble names its collection, and the bubble grid measures the overlay so the headline can change length without bubbles creeping under it.
  • Wallet list cut to four states, rows named (7fc4a29): the filter row offered six ways in (two asking the reader to tell a free score from a paid report); it now asks the one question a scanned wallet answers — can you open it — scored rows read Report and the scanned filter takes both. A Label column carries the wallet’s OpenSea name as its own non-blocking procedure (table renders without waiting on it). Also: the scan button arrives with the selection and says how much it will act on, whole-list work moved behind the dots, last-scanned reads as an age, the pager counts the set it’s paging, the score drops the denominator every site score shares, and the summary leads with what the list holds. On a phone the three onboarding steps take the whole screen and the last step’s button says Continue where a thumb sits.
  • Identity + authority polish: the account chip now says who you are (8094d43) — linked X picture + display name, each half falling back (address keeps the slot, the address-seeded glyph keeps the face) so it’s never bare. The public whitelist page now shows the manage door to the artist alone (6dfcbfa) — the “Artist manage” link renders only for the artist wallet, lifecycle buttons disable for anyone else with a line saying why (they previously rendered for all and silently no-op’d).
  • Credit ledger reworked (af8e43e/255e928/d8e9bdb/0a66bb5/52022af + 9be90e8): new CreditLedger.tsx (+159) + lib/credit-ledger.ts, the balance sums the whole ledger, not the fetched slice, the profile credit ledger was restored and its reuse simplified, and the score card’s parts were folded under the act with the evidence notice tucked in. Scan pricing + credit estimates were updated (9be90e8). Plus typed Effect failures kept typed on the backend (58a9d15), database-backed tests made to assert what they claim (9de155a), a post-merge scan-overview reconciliation (4ee972b), and a pre-deployment review-gap sweep (59993f6). Heavy new test footprint: agentBonus / scanOverviewLedger / shareUnlock / walletListIdentitiesRpc / walletScan integration + unit suites.

Open questions

  • Which skin a given route renders is now answerable by shell class: .protocol-shell (legacy sweeps/whitelists/admin) vs .a4f-shell (authenticated welcome/scan/profile). Whether any public/landing route still defaults to .protocol-shell vs the joyful skin was not exhaustively traced.
  • Exact agents4.fun domain string not found in-repo — RESOLVED 2026-07-18: the landing meta title now uses agents4.fun (0613c8f); the visible brand still renders “Agents 4.Fun” with a space.

Sources

  • ~/Documents/localhost/agent-wl/README.md, CLAUDE.md, design.md
  • agent-wl/docs/eligibility-and-tickets.md, docs/hook-abi.md, docs/requirements.md, docs/read-model-schema.md
  • agent-wl/packages/contracts/sweep/src/*.sol, packages/contracts/sweep/test/unit/*, .fdb/domain-language/contract-fee-split.md, .fdb/bounded-contexts/sweep-lifecycle.md
  • agent-wl/apps/web/package.json, apps/web/src/styles.css, apps/web/src/a4f.css, apps/web/src/routes/{landing,index,scan,admin.agent-collections,_protected}.tsx, apps/web/src/lib/wallet-profile.ts, commit 2538303 “new ui”
  • agent-wl/docs/wallet-score-spec.md, docs/agent-snapshot-analysis.md, packages/db/migrations/{0009_wallet_score,0010_agent_collection,0011_clear_agent_snapshot}.ts, scripts/wallet-score.sh
  • agent-wl/apps/backend/src/domain/wallet/{scanJobs,walletEvidence,walletScanReport,walletScan,walletScore,shareUnlock,walletProfile}.ts, apps/backend/src/{maintenance,scanWorker}.ts, apps/web/src/routes/{scan,scan_.jobs.$jobId,profile,welcome,index}.tsx, apps/web/src/routes/_protected*.tsx, apps/web/src/lib/auth-guard.ts, packages/db/migrations/{0012_agent_nft_metadata_cache,0013_twitter_account_constraints,0014_wallet_evidence,0015_resumable_scan_jobs,0016_scan_reconciliation_safety,0017_wallet_evidence_repair,0018_share_unlock}.ts
  • agent-wl/docs/onboarding/, .fdb/domain-language/{onboarding-journey,landing-surface,protected-app-shell}.md, docs/agent-minting-path.md, docs/agent-minting-all.md, .fdb/adrs/{chain-authoritative-holding-corrections-rca}.md, .fdb/code-rules/reconcile-holdings-without-inventing-history.md
  • agent-wl/apps/backend/src/domain/agents/{agentDiscovery,agentMetadataCache}.ts, apps/backend/src/domain/services/postgresAdvisoryLock.ts, apps/web/src/components/XAccountLink.tsx, apps/backend/src/authConfig.ts, .fdb/adrs/{resumable-scan-pricing,x-identity-linking,agent-discovery-layered-cache,incremental-wallet-source-cache,complete-wallet-score-snapshots}.md
  • agent-wl/apps/backend/src/domain/wallet/{scanState,scanProviderBudget}.ts, apps/backend/src/domain/admin/users.ts, apps/backend/src/rpc/admin.ts, apps/backend/src/api/errorMap.ts, apps/web/src/lib/auth-guard.ts, apps/web/src/routes/admin.users_.$userId.tsx, packages/db/migrations/{0023_admin_credit_actor,0024_incremental_scan_authorization}.ts, .fdb/adrs/{incremental-scan-authorization,admin-user-observability,strict-layered-deployment-configuration,test-deployment-mainnet-read-only,owner-only-vrf-timeout-recovery,whitelist-fees-use-pull-claims}.md, .fdb/domain-language/{scan-block,standard-scan-allowance,scan-credit-reservation,continuation-batch,deployment-configuration}.md, apps/indexer/ponder.config.ts, scripts/deploy/{provision,deploy,backup-databases}.sh (2026-07-16→18: v2 scan blocks e12d302/6b5ffbf/9e4e269, admin observability+credits ff87665/afbccc1/90c4b4e/3db5008, deploy/contract hardening 8250e24/5060f0a/d8dfa67/2a4011f/243b132, scan Wallet List UI a5cfd36)
  • agent-wl/apps/web/src/components/ScanWorkspace.tsx, apps/web/src/routes/{_protected.scan,_protected.scan.list,_protected.scan.runs,_protected.scan.runs.$actionId,index,agents,create,docs}.tsx, apps/backend/src/domain/wallet/{walletList,scanJobs,collectionPresentationCache,walletScan}.ts, apps/backend/src/scanWorker.ts, apps/web/src/components/{ProfileMasthead,credit,Header}.tsx, packages/db/migrations/{0025_wallet_list_action_jobs,0026_wallet_list_action_skip_reasons,0027_wallet_list_action_entries,0028_collection_presentation_cache}.ts, .fdb/adrs/{wallet-list-action-job-monitoring-link,shared-collection-presentation-cache,protected-nested-route-outlet-rca}.md, .fdb/domain-language/{wallet-list-action-entry,wallet-list-action-job-link,collection-presentation}.md, scripts/local-qa/{anvil,deploy,up,eth-price-refresher}.sh, apps/backend/scripts/refresh-wallet-scores.ts (2026-07-18→19: scan control-room eff5225/6e09121/14e79e7/fee2613, run-monitoring link table + grouped runs eff5225/8771066/f2b6d8e/2968508/ee4962f, shared presentation cache 551927a, profile consolidation 6bde458, repositioned copy 0613c8f, mainnet-fork local QA b8dd498/26c6ece)
  • agent-wl/apps/backend/src/domain/wallet/linkedWallets.ts, apps/backend/src/{authSiwe,auth}.ts, apps/backend/src/rpc/{router,sessionWallet,inputs,outputs}.ts, apps/web/src/components/LinkedWallets.tsx, apps/web/src/lib/wallet-auth.ts, apps/web/src/routes/_protected{,.profile}.tsx, packages/db/migrations/0029_multi_wallet_session.ts, .fdb/adrs/{linked-wallet-switching-and-unlinking,opensea-wallet-discovery-is-advisory}.md, .fdb/domain-language/wallet-link-suggestion.md (2026-07-19: multi-wallet account linking 9ca352d, merged 908137e)
  • agent-wl/apps/web/src/routes/{_protected.ui-kit,_protected.wallets,_protected.wallets_.$wallet,_protected.scan.$scanId}.tsx, apps/web/src/lib/{badgeTone,statusTone,uiKitAccess}.ts, apps/web/src/components/{StatusBadge,SweepStatusBadge}.tsx, apps/web/src/ui/dialog.tsx, apps/web/package.json (shadcn/Radix deps removed), deleted apps/web/src/components/ui/{badge,button,calendar,date-time-picker,dialog,label,popover,select,tabs}.tsx (WIP design-system consolidation — checkpoint a17bdca + uncommitted tree, 2026-07-20)
  • agent-wl/apps/web/src/routes/{_protected.wallets,_protected.wallets_.$wallet,_protected.scan,_protected.scan.$scanId}.tsx, apps/web/src/components/{ScanJobConsole,ScanWorkspace,ScanJobProgress}.tsx, apps/web/src/ui/dialog.tsx, apps/web/src/{a4f,styles}.css, apps/backend/src/domain/wallet/{walletList,walletProfile}.ts, apps/backend/src/rpc/router.ts (wallet-centric IA: scan console + report merged onto /wallets/$wallet, Wallets top-level nav, /scan/jobs/$jobId → redirect — 354f58d 07-21, 44b6e23 07-22; local agent-wl, branch dev)
  • ~/Documents/localhost/agent-wl/packages/agent-client/src/index.ts (createWhitelist), scripts/agent-create.mjs, apps/web/src/routes/{_protected.wl.create,_protected.wl.$chainId.$whitelistId_.manage,wl,wl.index,wl.$chainId.$whitelistId}.tsx, apps/backend chains API (standardHookAddress), packages/db/migrations/{0031_wallet_list_action_history,0032_koalified_agent_collection,0033_verify_curated_agent_collections}.ts, packages/db/src/{migrator,schema,agentCollectionSeed.integration.test,curatedVerificationBackfill.integration.test,migrationHistory.integration.test,renumberedLedgerRepair.integration.test}.ts, apps/web/src/routes/{_protected.wallets,_protected.wallets_.$wallet,_protected.scan*}.tsx, apps/web/src/components/{ScanMini,GlobalScanMini}, docs/wallet-score-spec.md, scripts/local-qa/e2e-sweep.sh (agent whitelist creation + partner collections 8519f91/be026ae/e523e04/27a2a09, score curated/bought holdings 027b11e, scan→Wallets collapse + floating widget f5a83e0/8ed1f2d/0963f71/5441eb4/4d4ad4e, ledger repair + test isolation 98e27e0/dd1253e — 2026-07-22→25; local agent-wl, branch dev)
  • ~/Documents/localhost/agent-wl/apps/backend/src/rpc/sessionWallet.ts, apps/web/src/lib/auth-guard.ts, apps/web/src/styles.css, apps/web/vite.config.ts, CLAUDE.md (onboarding/admin acting-wallet fix + landing-overlay CSS c04d02d, SSR better-auth externalization 754c330, deploy runbook f7b92e5 — 2026-07-25; local agent-wl, branch dev)
  • ~/Documents/localhost/agent-wl/apps/backend/src/domain/wallet/{walletScore,walletEvidence,walletScan,walletProfile,walletMarketData,walletInsights,walletList,walletScanReport,scanState,collectionPresentationCache}.ts, apps/backend/src/{scanWorker,rpc/sessionWallet}.ts, apps/web/src/ui/{menu,progress,button}.tsx, apps/web/src/components/{Header,ProfileMasthead,ScanMini,ScanWorkspace,ScanJobProgress,LinkedWallets,ScoreRing,WalletReport}.tsx, apps/web/src/routes/{_protected.profile,_protected.wallets,_protected.ui-kit,_protected.wl.create}.tsx, apps/web/src/{a4f,styles}.css, apps/web/src/assets/brand/a4f-*.svg, apps/web/public/favicon*.{png,svg}, apps/web/src/lib/batch-progress.ts, packages/db/migrations/{0036,0037} (test-only wallet_score clear), and .fdb/adrs/{collector-reputation-scoring-semantics(deprecated),wallet-score-fixed-base-and-quick-flip-penalty(deprecated→v8),wallet-score-quick-flip-penalty-cap(accepted/v8),wallet-score-reliable-subset-with-exclusions,nft-burn-scoring-rca,quantity-aware-wallet-evidence,chain-authoritative-holding-corrections-rca,advisory-wallet-market-values-and-trending}.md, .fdb/code-rules/{nft-burns-are-score-inert,reconcile-holdings-without-inventing-history}.md, .fdb/domain-language/{collector-reputation-score,wallet-score-positive-base,long-term-holding-credit,quick-flip-penalty,observed-hold-duration,sale-hold-duration,wallet-portfolio-estimate}.md, .fdb/todo/grill-wallet-score-v7-regression-wallets.md (Wallet Score formula overhaul v6→v8 + brand/UI polish, ff079bacc70cb538500f7353c71248cb3ae32267d5, 2026-07-26→27; local agent-wl, branch dev, HEAD 32267d5)
  • ~/Documents/localhost/agent-wl/apps/backend/src/domain/wallet/{walletIdentity,agentBonus,shareCard,walletScore,walletList,walletProfile}.ts, apps/backend/src/api/shareCardRoute.ts, apps/backend/src/scanWorker.ts, apps/backend/scripts/rescore-priced-wallets.ts, apps/backend/src/rpc/router.ts, apps/web/src/components/{AddressGlyph,WalletMasthead,ProfileMasthead,ScoreRing,ScanJobConsole,ScanJobProgress,ScanMini,WalletReport,ShareCardDialog,ShareScoreButton,credit,ScanWorkspace}.tsx, apps/web/src/routes/{_protected.wallets,_protected.wallets_.$wallet,_protected.profile,index}.tsx, apps/web/src/ui/{dialog,menu}.tsx, apps/web/src/{a4f,styles}.css, apps/web/src/lib/{formatters,wallet-profile,wallet}.tsx, apps/web/server.mjs, apps/web/package.json, apps/web/public/{agents4fun-logo.png,fonts/*}, scripts/deploy/deploy.sh, scripts/local-qa/{START-SERVER.md,seed-agent-art.sh} (public wallet profiles + address glyph, phone-fit UX pass, unified modal footer, agent-holder 0.01 ETH share-card bonus + satori card render/serve fixes, share-from-profile, re-score-on-priced + floor labelling, paid-scan breakdown unlock, list liveness + delete confirm — 80b52979f7aa19a92512cca67b349b74a12e3d6ec58ee219c05b2a04a79f44fe18056ebba6b79d55e870cb3bb2984f78f98b09091e91195ea24dd18c4bc013, 2026-07-27→28; local agent-wl, branch dev, HEAD c4bc013; uncommitted WIP: apps/web/src/ui/{chip-group,choice-cards,field,segmented,stepper,tile-group,toggle-row,selection-controls})
  • ~/Documents/localhost/agent-wl/apps/backend/src/api/{agentActions,agentManifest,agentDiscovery,mcp,handlers,definition,schemas}.ts, apps/backend/src/domain/agents/agentDiscovery.ts, apps/backend/src/domain/{cache,effect,errors,schema}.ts, apps/backend/src/domain/services/{config,ponderReadModel}.ts, apps/backend/src/domain/wallet/{shareUnlock,walletScan,walletList,walletMarketData}.ts (deleted domain/{manifestActions,agentManifest,manifest}.ts + usecases/getAgentManifest.ts), apps/web/src/ui/{chip-group,choice-cards,field,segmented,stepper,tile-group,toggle-row,selection-controls.test}.tsx, apps/web/src/routes/{_protected.wl.create,wl.$chainId.$whitelistId,_protected.wl.$chainId.$whitelistId_.manage,_protected.wallets,_protected.profile,_protected.ui-kit,index}.tsx, apps/web/src/components/{CreditLedger,WalletMasthead,WalletReport,ProfileMasthead,ScoreBreakdown,ShareCardDialog,ShareScoreButton,credit}.tsx, apps/web/src/lib/{credit-ledger,formatters,statusTone,wallet}.ts(x), apps/web/src/{theme,styles,a4f}.css, .github/workflows/ci.yml (NEW — first CI), LLM.md (new), .mcp.json, apps/backend/src/tests/{agentBonus,scanOverviewLedger,shareUnlock,walletListIdentitiesRpc,walletScan,api.handlers,cache,regressions}.test.ts, and .fdb/adrs/{canonical-agent-entrypoint,agent-holding-bonus,share-gated-score-breakdown,x-identity-linking}.md (selection-control primitives shipped + unified /agent.md canonical agent entrypoint & role-scoped MCP + agent-holder-bonus/X-share-proof ADRs + landing gate redesign + wallet-list 4-states + first CI catching a v3-report 500 — 52022afc4c10a56cb4201bab4ca27fc4a29b988de153e63908094d436dfcbfa59993f6, 2026-07-28; local agent-wl, branch dev, HEAD 59993f6)
  • Related: agent-x402-payments
  • Related: depunks, opensea-erc8217, agent-rep-trust, raw/agent-wl-sweep-notes