feat: live x402 + MPP weather agent, deployable, wired into the tutorials showcase - #69
Conversation
…lStorage) drives live x402/MPP - Connect button redirects to the App /auth/cli?callback_url=...; callback returns ?nvm_api_key, stored in localStorage and sent per-request; server mints tokens with the viewer's own key. - Security: shared-key env fallback gated behind ALLOW_SHARED_KEY_DEV (prod = per-user only); key lives in localStorage not the cookie (CSRF can't trigger spend); key never logged.
…code links in live panel
…y-as-you-go MPP tutorial - Under the hood: optional grouped code samples rendered as numbered sub-sections; http-simple-agent-ts now shows real Client + Agent (server) code, keyless dual-protocol. - New MPP protocol category + 'Pay per forecast over MPP' tutorial (live, /weather/payg, PAYG pricing). - live buyer: mpp-weather-payg slug, parses forecast days, formats multi-day forecasts; py tutorial reverts to simulator.
… count The MPP/PAYG tutorial priced every request at a flat '1 credit'. Now the live buyer computes the actual per-request cost (1 for today, up to 7 for a forecast), the 402 reads 'Pay-as-you-go · N credit(s) for this request', settle notes '(pay-as-you-go)', and the balance draws down by the real amount.
…tirely Pay-as-you-go has no credit pool: the 402 reads 'Payment required · pay-as-you-go', the receipt reads 'paid · pay-as-you-go', and no credit balance is shown or tracked. Fixed-credit tutorials still show credits + balance.
|
👀 Reviewing |
r-marques
left a comment
There was a problem hiding this comment.
🤖 Automated PR review — 🔴 Blocked
Adds a live x402 + MPP weather agent (three plans, three routes, mpp: true on each) and wires two showcase panels to it as a real buyer using the viewer's own Nevermined key. The core design is sound and the money path holds up: I traced pricing end to end and found no mismatch between what is charged and what is delivered. The blocker is not in the new code — it is that http-simple-agent-ts/README.md was left describing the agent this PR replaced, in a repo where the README is the deliverable.
What I reviewed
- Diff range
origin/main...HEAD— 31 files, +8325/−600 (≈1,600 lines excludingpackage-lock.json,yarn.lockand the plan doc), head82aace8b. - Panel: none. Per the standing instruction not to spawn subagents unless asked, this is my own source-level review; every finding below was verified by reading code, not PR prose.
- Verified the settle gate rather than assuming it. Pulled the pinned
@nevermined-io/payments@1.11.2tarball and readdist/x402/express/middleware.js: both rails bill only onres.statusCode >= 200 && < 300and a delivery probe (:894MPP,:1094x402). So the 400 fromparseWeatherRequestand the 404 fromCityNotFoundErrornever settle — the "charged for a validation failure" bug I went looking for does not exist here. - Confirmed both SDK features the PR leans on are real in 1.11.2:
RouteConfig.creditsaccepts(req, res) => number | Promise<number>, andmpp: trueis valid (MppRouteOption). The pin is also the current latest. - Checked pay-as-you-go for a price/delivery split:
priceForRequestandgetForecastboth clampdaysto1..7viaMath.max(1, Math.min(7, Math.trunc(n))), so charge and payload agree at every input I traced (0,0.5,1.9,-5,100,NaN). - Traced the unsigned
nvm_democookie into the live path —route.ts:19warns it "must never gate real spend". It does reachliveRespond, but every purchase runs on the viewer's own key and the delegation is created lazily insidebuyX402/buyMppregardless, so forgingauthorized/balancemoves a UX counter, not money. The shared-key fallback is correctly default-off behindALLOW_SHARED_KEY_DEV=1. - Stood down on a deployment concern after reading the docs. The prod manifest pairs
automated: {prune, selfHeal}with a placeholderimage.tag: sha-REPLACE_AFTER_FIRST_CI_BUILD, which would be a merge-time breakage — exceptdeploy/argocd/README.mdstates these files are reference-only and not consumed by ArgoCD from this repo. Not an issue; noted so the next reader does not re-raise it. - Verified the deploy README's
replicaCount: 1claim against the SDK's own comments oninFlightMppCredentials/spentMppCredentials. The constraint is genuine; only its attribution is wrong (nit below).
Verdict: 🔴 Blocked — 1 blocker, 5 should-fix, 2 nits.
The blocker is a documentation gap, not a defect in the new agent, so it should be quick: bring README.md in line with the routes, env vars and scripts this PR ships, and decide whether yarn client is kept or dropped.
🧭 Inline comments (8)
Each is posted on its line in Files changed.
- 🔴 BLOCKER — README still documents the removed /ask endpoint, env vars and scripts —
http-simple-agent-ts/src/agent.ts:49 - 🟡 SHOULD FIX —
yarn clientis still advertised but now targets a removed route and env var —http-simple-agent-ts/tsconfig.json:13 - 🟡 SHOULD FIX — Missing NVM_RECEIVER silently registers three FREE plans —
http-simple-agent-ts/scripts/register-plans.ts:14 - 🟡 SHOULD FIX — Cached delegation outlives its own 7-day expiry, with no recovery path —
showcase/lib/live-agent.mjs:78 - 🟡 SHOULD FIX — Viewer's API key arrives as a URL query param, so it lands in server access logs —
showcase/components/LiveRunPanel.tsx:76 - 🟡 SHOULD FIX — Tutorial teaches the deprecated
environmentoption —http-simple-agent-ts/src/agent.ts:35 - 💡 NIT — MPP single-use guard is attributed to agent.ts; it lives in the SDK middleware —
http-simple-agent-ts/deploy/argocd/README.md:68 - 💡 NIT — cityOf can lift a leading capitalised non-city word —
showcase/lib/live-agent.mjs:92
r-marques
left a comment
There was a problem hiding this comment.
🔴 Blocked — 1 blocker(s) to address before merge. Details are inline on the changed lines.
Inline review: #69 (review)
- BLOCKER: rewrite http-simple-agent-ts/README.md for the weather x402+MPP agent (routes, PLAN_ID_* env, scripts). - Drop stale LLM-era client.ts + agent-observability.ts (broke `yarn client`); remove the client script + tsconfig excludes. - register-plans: warn loudly when NVM_RECEIVER unset (no silent FREE plans); add NVM_RECEIVER/PLAN_PRICE_WEI to .env.example. - Drop deprecated `environment` option (agent.ts + register-plans.ts); remove NVM_ENVIRONMENT. - live-agent: delegation TTL + re-mint past expiry + invalidate on expiry error; bound the per-key caches; scale delegation limit to $5; cityOf takes the last non-stopword capitalised token. - Read the connect key from URL fragment too (forward-compat for the App-side log fix); strip fragment on cleanup. - deploy README: correct MPP-guard attribution (SDK middleware, not agent.ts); document ~15-20s boot probe timing + API-key-in-logs exposure.
|
All review comments addressed in
One item only partially in this repo's control: the viewer key arrives as a query param from the App's |
r-marques
left a comment
There was a problem hiding this comment.
🤖 Automated PR review (round 2) — 🟡 Mergeable with nits
All 8 round-1 findings are closed. I checked each against the code at fb39059a rather than against the commit message:
| # | Round-1 finding | Status |
|---|---|---|
| 1 | 🔴 README documented the removed /ask, env vars and scripts |
Fixed — README rewritten; its route table matches agent.ts:76-79 exactly, and its script table lists all 7 package.json scripts with no strays |
| 2 | 🟡 yarn client advertised a removed route |
Fixed — src/client.ts deleted, client script removed, tsconfig exclude cleaned up |
| 3 | 🟡 Missing NVM_RECEIVER silently registered FREE plans |
Fixed — loud ⚠ NVM_RECEIVER is unset — registering FREE plans … NO payment will be charged (register-plans.ts:14), and it is now documented in both .env.example and the README |
| 4 | 🟡 Cached delegation outlived its 7-day expiry | Fixed, and then some — cache is {id, expiresAt} with a 60s safety margin, plus invalidateDelegation() on a delegation-rejected error (live-agent.mjs:253), which is the recovery path I asked for |
| 5 | 🟡 Viewer API key in a URL query param → access logs | Resolved as documented — capture reads query and fragment so the App-side fix needs no change here, and the residual exposure now has its own section in deploy/argocd/README.md:84 with two remediations. Non-unilateral fix, knowingly accepted — reasonable |
| 6 | 🟡 Tutorial taught the deprecated environment option |
Fixed in agent.ts — see the one carry-over below |
| 7 | 💡 MPP guard misattributed to agent.ts |
Fixed — now correctly credited to paymentMiddleware's inFlightMppCredentials / spentMppCredentials |
| 8 | 💡 cityOf lifted a leading capitalised non-city word |
Fixed — stopword set + last-capitalised-word; "How is Madrid looking?" now resolves to Madrid |
Two improvements landed that I had not asked for and that are worth calling out: the showcase delegation cap dropped from $100 → $5 with the reasoning recorded inline ("on the VIEWER's account"), and both per-key caches are now bounded (MAX_KEYS = 200) instead of growing for the process lifetime.
The one carry-over is finding 6's twin: agent.ts dropped the deprecated environment option, but scripts/smoke.ts — which the README now tells users to run — still passes it, reading an env var this PR removed from .env.example.
What I reviewed
- Full diff
origin/main...HEAD— 33 files, +8503/−1289, headfb39059a. - Delta since my round-1 review (
82aace8b..fb39059a) line by line — 11 files, +210/−721. - Panel: none — reviewed directly (this session's standing instruction is not to spawn subagents).
- Swept for dangling references after the two file deletions: no remaining
agent-observability/src/client.ts/yarn clientreference anywhere inhttp-simple-agent-tsorshowcase. The hits inhttp-simple-agent-py/**andshowcase/content/tutorials.ts:250are the Python tutorial's ownclient.pyand are correctly untouched; the repo-rootCLAUDE.mdmentionsyarn clientonly as a generic "most tutorials" example, which is still true of the Python one, so I am not flagging it. - Confirmed
DockerfileENTRYPOINT ["yarn","start"]→node dist/agent.jsmatches thestartscript, and thattsconfig(include: src/**/*) deliberately leavesscripts/totsx.
Verdict: 🟡 Mergeable with nits — 0 blockers, 1 should-fix, 2 nits.
🧭 Inline comments (3)
Each is posted on its line in Files changed.
- 🟡 SHOULD FIX — smoke.ts still passes the deprecated
environmentoption, reading an env var this PR removed —http-simple-agent-ts/scripts/smoke.ts:9 - 💡 NIT — The
in <city>branch swallows trailing words, so "weather in Tokyo today" 404s —showcase/lib/live-agent.mjs:111 - 💡 NIT — Runbook still sets NVM_ENVIRONMENT, which the agent no longer reads —
http-simple-agent-ts/deploy/argocd/README.md:129
r-marques
left a comment
There was a problem hiding this comment.
🟡 Approved with comments — no blockers; 1 should-fix, 2 nit(s) left inline.
Inline review: #69 (review)
- smoke.ts: drop the deprecated `environment` option + NVM_ENVIRONMENT (the twin the round-1 fix missed). - cityOf: bound the 'in <city>' capture so 'in Tokyo today' -> Tokyo while 'New York' still works. - Remove dead NVM_ENVIRONMENT from both deploy manifests and the runbook (helm --set + rendered snippet).
Why this matters
Our x402 tutorials looked interactive but were hitting a local simulator — no visitor could watch a real payment happen, and we had no live MPP example at all. This PR ships a real, always-on weather agent that charges over both x402 and MPP, and wires the tutorials site to it: a visitor connects their Nevermined sandbox account and watches an actual payment settle for a real weather forecast. It gives us the first end-to-end, clickable demo of both payment protocols against a deployed Nevermined agent — the thing a developer evaluating Nevermined can try in one click.
What's in this PR
A dual-protocol weather agent (
http-simple-agent-ts, re-implemented)paymentMiddleware, three routes, each speaking x402 AND MPP (mpp: true):/weather/credits(fixed credits),/weather/subscription(time),/weather/payg(pay-as-you-go).priceForRequest+parseWeatherRequestwith assert self-checks.scripts/register-plans.ts(creates the three plans) andscripts/smoke.ts(x402 + MPP buyer round-trips).Deployment (agents namespace)
Dockerfile(node:22-alpine) — builds, runs, serves; verified end-to-end.http-simple-agent-ts/deploy/argocd/, plus a README documenting the one required chart patch (addPLAN_ID_*env lines) and the deploy steps. Deploy itself is handed off separately.The tutorials site consumes the live agent (
showcase)/auth/clireturns a sandbox API key to?nvm_api_key, stored in localStorage; the server-side buyer pays with the viewer's own key (never in the browser beyond localStorage)./api/agent); simulator remains the fallback for unwired tutorials.Test plan
yarn build(agent) green;yarn pricing:selfcheck/yarn request:selfcheckpass.docker build+ run:/healthOK, unpaid request → 402 advertising bothpayment-required(x402) andWWW-Authenticate: Payment(MPP). Boots in ~15s (deploy probes must allow for this).payment-responsereceipt, 1 credit burned). MPP completes the challenge→credential handshake and serves real weather.next buildtype-checks the content array; Connect flow + live x402/MPP verified through/api/agent; PAYG shows no credits/balance; x402 credits tutorial unchanged.settleCredential→BCK.MPP.0003), reproducible independent of route while x402 settles fine — likely the same-account buyer==seller case (single test key). To confirm with a distinct buyer account and/or on the SDK side.PLAN_ID_*patch, GCP secret, plan IDs, first CI sha, argocd merge) — handed off to a separate agent.🤖 Generated with Claude Code
https://claude.ai/code/session_0129cTdzZEG2iLnxsVBf3LQr