Skip to content

feat(web): Moonwell Score — public viewer for the credit-report capability#17

Open
imthatcarlos wants to merge 10 commits into
mainfrom
feat/moonwell-score
Open

feat(web): Moonwell Score — public viewer for the credit-report capability#17
imthatcarlos wants to merge 10 commits into
mainfrom
feat/moonwell-score

Conversation

@imthatcarlos
Copy link
Copy Markdown
Collaborator

@imthatcarlos imthatcarlos commented May 26, 2026

Summary

A shareable /score page for the Moonwell credit-report capability, plus a dynamic per-subject share card for social unfurls. Onchain credit for agents. Powered by Moonwell.

What's in it

/score pagepackages/web (static export)

  • ENS / Basename / 0x input → editorial-mono score card: score (300–850), tier, strategy fingerprint, health factor, components, activity, disclaimers (verbatim per SKILL.md).
  • /score/compare?a=&b= — side-by-side with a derived one-line verdict.
  • "Post to X" + copy-link; clean "no record yet" fallback for unscored wallets.
  • The card shown on the page is the rendered share-card image — zero drift with what unfurls socially.

Share cardpackages/share-card (new Cloudflare Worker)

  • GET /og/:subject → 1200×630 PNG via Satori (workers-og): a grounded semicircular FICO-style gauge (muted track, only your tier colored, marker at your score), the score + tier nested in the hollow, Moonwell logomark, tier moon-phase, strategy + health, resolved ENS/Basename, bordered frame.
  • GET /s/:subject → HTML landing with absolute per-subject OG/Twitter meta + a bot-gated redirect to the live page.
  • Edge-cached (SWR), no secrets — pulls data from api.moonwell.fi/v1/credit-report.

APIpackages/api

  • Adds a /v1/rpc/:chain proxy so the score page resolves ENS/Basenames without exposing an auth'd RPC URL in the browser.

Deploy checklist (needs Cloudflare access)

⚠️ The /score card image points at agents.moonwell.fi/og/*, so deploy the share-card worker + routes before/with the web deploy or the card 404s. Nothing auto-deploys on merge.

  • Deploy the api Worker — pnpm deploy:api (ships the new /v1/rpc route)
  • Set the RPC secret — from packages/api: wrangler secret put MAINNET_RPC_URLhttps://rpc.moonwell.fi/main/evm/1
  • Deploy the share-card Worker — pnpm deploy:share (no secrets)
  • Add Worker Routes agents.moonwell.fi/s/* and agents.moonwell.fi/og/* → the share-card worker (must take precedence over the static-assets worker)
  • Verify a social unfurl — paste https://agents.moonwell.fi/s/<address> into X / Discord / opengraph.xyz

Follow-ups (non-blocking)

  • ENS/Basename resolution inside the share-card worker (currently address-only; the in-app flow always passes the resolved address)
  • Farcaster frame meta, save-image dialog, compare-card image

Test plan

  • pnpm dev:web + pnpm dev:share (set NEXT_PUBLIC_SHARE_BASE=http://localhost:8788)
  • /score?demo=1 — card image renders; /score?s=<addr> — live, "Post to X" posts the /s/ link
  • pnpm --filter @moonwell-ai/share-card test + typecheck green
  • pnpm build (web) clean

🤖 Generated with Claude Code

imthatcarlos and others added 5 commits May 22, 2026 11:32
A self-contained, shareable surface for the new credit-report capability —
turns the terminal-only score into something people can drop in an X reply.

Adds:
- /score — empty-state hero with ENS / Basename / 0x input + curated example
  links. Reads ?s=<input> and renders the editorial monospace score card,
  components breakdown, health-factor stats, activity summary, and the
  report's disclaimers (surfaced per SKILL.md).
- /score/compare — side-by-side cards for two wallets via ?a=&b=, with a
  derived one-line verdict ("X scores higher", "X carries less risk", …)
  designed for quote-tweets.
- Pre-filled X intent share button + copy-link, with three rotated tweet
  templates so concurrent shares don't read identically.
- Insufficient-history fallback (calm "no record yet" state) so wallets that
  have never used Moonwell still produce a meaningful page rather than a 404.
- Client-side ENS + Basenames resolution via viem (added as a web dep);
  Universal Resolver hit at the time of submit so URLs keep human names.

Surface-only — the root agents.moonwell.fi landing page is intentionally
untouched, and the credit-report API at api.moonwell.fi is consumed unchanged.

Constraint: agents.moonwell.fi runs as Workers Static Assets (output:export),
so per-subject dynamic OG images can't be generated at request time. v1.1
will add a thin Cloudflare Worker companion to render Satori-driven PNGs
keyed on subject; for v1, the page emits a generic Moonwell Score card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l favicon

Addressing review feedback on the initial /score surface:

- Basenames resolution was broken because viem's Base chain config has no
  ensUniversalResolver. Drop the standalone Base client and resolve every
  name (including *.base.eth) through the mainnet Universal Resolver, which
  follows the L1 Resolver's CCIP-Read pointer to the L2 record. Swap the
  RPC to publicnode — cloudflare-eth.com currently reverts on
  resolveWithGateways and breaks the offchain hop.

- Extract SiteNav and SiteFooter into shared components so the root
  landing and /score render the same header/footer. Adds a "Score" link to
  the root nav and an "Agents" link on /score; brand suffix flips between
  Agents / Score so users still know where they are.

- Mount LunarTerrain on the /score and /score/compare surfaces so the
  background reads as the same site as the root landing.

- Tighten the empty-state copy. "Score any wallet on Moonwell." replaces
  the wordier hero; description trimmed to a single line that names the
  three signals (score, tier, fingerprint) without listing chains in body
  prose.

- Replace the stock Next favicon with a Moonwell-branded one. icon.svg in
  the App Router conventions slot drives modern browsers and tab tabs.
  apple-icon.png covers iOS home-screen pinning; generated via a one-shot
  sharp script (scripts/generate-apple-icon.mjs) so the SVG remains the
  source of truth.

- Wire ?demo=1 on /score to load lib/sample-report.json (a snapshot of
  the production credit-report response for 0x1a1E…389F9) — lets us walk
  the populated card without burning API requests during local design
  passes. Easy to drop pre-merge if we don't want the fixture in the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the in-repo generated SVG favicon for the canonical assets served by
moonwell.fi:

- favicon.ico (32x32 ICO) pulled from https://moonwell.fi/favicon.ico
- apple-icon.png resized from https://moonwell.fi/apple-touch-icon.png
  (800x800 source → 180x180 to match iOS home-screen convention)

Drop the now-unneeded icon.svg and the sharp-based generator script —
moonwell.fi is the source of truth, refresh by re-fetching if it ever
changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion commit to the previous deletion — adds the actual official
binaries from moonwell.fi that Next.js will emit as the site icons:

- favicon.ico — 32x32 from https://moonwell.fi/favicon.ico
- apple-icon.png — resized from https://moonwell.fi/apple-touch-icon.png
  to 180x180 for iOS home-screen pinning

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 26, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
moonwell-api de789f3 May 26 2026, 04:22 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 26, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
moonwell-ai de789f3 May 26 2026, 04:22 PM

imthatcarlos and others added 2 commits May 26, 2026 11:21
The score page at agents.moonwell.fi/score needs a mainnet RPC for ENS /
Basenames resolution. Public RPCs (cloudflare-eth, publicnode) are flaky
under load and don't always carry the CCIP-Read gateway hop reliably.
This adds a thin pass-through inside the api Worker so the browser can hit
api.moonwell.fi/v1/rpc/mainnet (and /base) without ever seeing the upstream
auth — paid provider keys stay in the Worker secret store.

Surface:
  POST /v1/rpc/mainnet
  POST /v1/rpc/base
  body  { jsonrpc: "2.0", id, method, params }

Constraints (deliberate):
  - Read methods only. eth_sendTransaction etc. rejected with -32601 so the
    proxy can't be turned into a free relay.
  - One request per call — no batch. Adds parsing surface for negligible
    wire benefit.
  - Upstream HTTP error bodies are never echoed. Providers leak stack
    fragments and sometimes the RPC URL with embedded key — log
    server-side, return a generic 502.
  - JSON-RPC error responses ARE forwarded verbatim so viem can read
    error.code for CCIP-Read OffchainLookup, execution reverts, etc.

New env binding: MAINNET_RPC_URL (set via `wrangler secret put`). The
existing BASE_RPC_URL is reused. Docs in wrangler.jsonc updated.

Tests: 9 new cases cover the chain allow-list, method allow-list,
GET rejection, missing-config 503, unreachable-upstream 503, verbatim
success forwarding, verbatim JSON-RPC error forwarding, and the critical
'do not echo upstream HTTP body' security property.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the score page's mainnet viem client from a public RPC
(ethereum-rpc.publicnode.com) to the new Moonwell-controlled proxy in the
api Worker. Honours NEXT_PUBLIC_MOONWELL_API_URL (already used by the
credit-report client) so local dev points at a local Worker when set,
production resolves to api.moonwell.fi.

Pre-requisite for shipping: set MAINNET_RPC_URL in the api Worker
(`pnpm exec wrangler secret put MAINNET_RPC_URL`). Until that's set the
proxy returns 503 and the score page surfaces a 'resolver error' on any
ENS / Basename input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
imthatcarlos added a commit that referenced this pull request May 28, 2026
Bring the Moonwell-branded site icons onto this branch — the same
binaries as PR #17 (commit 46b0ee0, sourced from moonwell.fi).
Replaces the default Next.js favicon and adds the apple-touch-icon;
Next.js auto-emits both from src/app/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
imthatcarlos and others added 2 commits May 28, 2026 15:14
New Cloudflare Worker (packages/share-card) that renders the score as a
social share card, plus wiring on the /score page.

- GET /og/:subject -> 1200x630 PNG via Satori (workers-og): semicircular
  FICO-style gauge (grounded track, only the active tier colored), score +
  tier in the hollow, Moonwell logomark, tier moon-phase, strategy + health,
  resolved ENS/Basename subject, bordered frame.
- GET /s/:subject -> HTML landing with absolute per-subject OG/Twitter meta
  + a bot-gated redirect to the live score page.
- /score renders the card image (no component drift) and shares the /s/ link;
  share-copy gains landingUrlFor/cardImageUrlFor (?name= passthrough).
- Edge-cached (SWR), no secrets — data via api.moonwell.fi/v1/credit-report.
- Root scripts: dev:share / deploy:share.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@imthatcarlos imthatcarlos requested a review from apokusin May 29, 2026 18:20
Fill the tier color from 300 up to the score (muted remainder + marker
at the score), instead of coloring only the tier's band at the far end.
Drops the FICO band-edge approximation — it's now a pure progress fill to
the real score, colored by the API tier.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@imthatcarlos
Copy link
Copy Markdown
Collaborator Author

Code review

Found 1 issue:

  1. LoansShell adds the /loans nav link, but ScoreShell and root page.tsx are not updated — so visitors on /score or / have no nav path to /loans. The SiteNav is described as shared across surfaces, and the same pattern was applied when /score was introduced (it added itself to all shells at once).

<SiteNav
brand={{ href: "/#top", label: "Moonwell", accent: "Agents" }}
links={[
{ href: "/", label: "Agents" },
{ href: "/score", label: "Score" },
{ href: "/loans", label: "Loans" },
{ href: "/skill.md", label: "Skill", external: true },
]}
/>

For reference, ScoreShell (unchanged in this diff) currently has:

<SiteNav
brand={{ href: '/#top', label: 'Moonwell', accent: 'Agents' }}
links={[
{ href: '/', label: 'Agents' },
{ href: '/score', label: 'Score' },
{ href: '/skill.md', label: 'Skill', external: true },

And root page.tsx:

brand={{ href: '#top', label: 'Moonwell', accent: 'Agents' }}
links={[
{ href: '/', label: 'Agents' },
{ href: '/score', label: 'Score' },
{ href: '/skill.md', label: 'Skill', external: true },
]}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant