Skip to content

feat: live x402 + MPP weather agent, deployable, wired into the tutorials showcase - #69

Merged
aaitor merged 24 commits into
mainfrom
aaitor/x402_mpp_tutorials
Sep 5, 2026
Merged

feat: live x402 + MPP weather agent, deployable, wired into the tutorials showcase#69
aaitor merged 24 commits into
mainfrom
aaitor/x402_mpp_tutorials

Conversation

@aaitor

@aaitor aaitor commented Sep 4, 2026

Copy link
Copy Markdown
Member

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)

  • One paymentMiddleware, three routes, each speaking x402 AND MPP (mpp: true): /weather/credits (fixed credits), /weather/subscription (time), /weather/payg (pay-as-you-go).
  • Keyless weather from Open-Meteo (no OpenAI). Boundary validation → 400. Pure priceForRequest + parseWeatherRequest with assert self-checks.
  • scripts/register-plans.ts (creates the three plans) and scripts/smoke.ts (x402 + MPP buyer round-trips).

Deployment (agents namespace)

  • Dockerfile (node:22-alpine) — builds, runs, serves; verified end-to-end.
  • CI workflow → GCP Artifact Registry via WIF (mirrors the showcase image pipeline).
  • ArgoCD Application manifests (prod + staging) in http-simple-agent-ts/deploy/argocd/, plus a README documenting the one required chart patch (add PLAN_ID_* env lines) and the deploy steps. Deploy itself is handed off separately.

The tutorials site consumes the live agent (showcase)

  • Connect with Nevermined: a per-visitor flow — the App's /auth/cli returns 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).
  • Real x402 + MPP purchases from the UI (server-side buyer proxy in /api/agent); simulator remains the fallback for unwired tutorials.
  • New MPP category + "Pay per forecast over MPP" tutorial (pay-as-you-go, no credit pool).
  • "Under the hood" split into 3.1 Client / 3.2 Agent with real code + links to the server source.
  • Signal "Connect" button (from the design), compact 402 row, in-flight spinners, MPP category glyph.

Test plan

  • yarn build (agent) green; yarn pricing:selfcheck / yarn request:selfcheck pass.
  • docker build + run: /health OK, unpaid request → 402 advertising both payment-required (x402) and WWW-Authenticate: Payment (MPP). Boots in ~15s (deploy probes must allow for this).
  • Live buyer against the real sandbox: x402 fully settles (real weather + payment-response receipt, 1 credit burned). MPP completes the challenge→credential handshake and serves real weather.
  • Showcase: next build type-checks the content array; Connect flow + live x402/MPP verified through /api/agent; PAYG shows no credits/balance; x402 credits tutorial unchanged.
  • Known follow-up: MPP settlement is rejected on sandbox (verify+serve works; settleCredentialBCK.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.
  • Deploy (chart 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

…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.
…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.
@r-marques

Copy link
Copy Markdown
Member

👀 Reviewing

@r-marques r-marques left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 excluding package-lock.json, yarn.lock and the plan doc), head 82aace8b.
  • 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.2 tarball and read dist/x402/express/middleware.js: both rails bill only on res.statusCode >= 200 && < 300 and a delivery probe (:894 MPP, :1094 x402). So the 400 from parseWeatherRequest and the 404 from CityNotFoundError never 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.credits accepts (req, res) => number | Promise<number>, and mpp: true is valid (MppRouteOption). The pin is also the current latest.
  • Checked pay-as-you-go for a price/delivery split: priceForRequest and getForecast both clamp days to 1..7 via Math.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_demo cookie into the live path — route.ts:19 warns it "must never gate real spend". It does reach liveRespond, but every purchase runs on the viewer's own key and the delegation is created lazily inside buyX402/buyMpp regardless, so forging authorized/balance moves a UX counter, not money. The shared-key fallback is correctly default-off behind ALLOW_SHARED_KEY_DEV=1.
  • Stood down on a deployment concern after reading the docs. The prod manifest pairs automated: {prune, selfHeal} with a placeholder image.tag: sha-REPLACE_AFTER_FIRST_CI_BUILD, which would be a merge-time breakage — except deploy/argocd/README.md states 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: 1 claim against the SDK's own comments on inFlightMppCredentials / 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 FIXyarn client is 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 environment option — 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

Comment thread http-simple-agent-ts/src/agent.ts
Comment thread http-simple-agent-ts/tsconfig.json Outdated
Comment thread http-simple-agent-ts/scripts/register-plans.ts
Comment thread showcase/lib/live-agent.mjs Outdated
Comment thread showcase/components/LiveRunPanel.tsx Outdated
Comment thread http-simple-agent-ts/src/agent.ts Outdated
Comment thread http-simple-agent-ts/deploy/argocd/README.md Outdated
Comment thread showcase/lib/live-agent.mjs Outdated

@r-marques r-marques left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.
@aaitor

aaitor commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

All review comments addressed in fb39059 (pushed) — replied inline and resolved all 8 threads (1 blocker, 5 should-fix, 2 nits).

  • 🔴 README rewritten for the weather x402+MPP agent (routes, PLAN_ID_*, scripts).
  • Dropped stale client.ts + agent-observability.ts (fixes the broken yarn client).
  • register-plans warns loudly on missing NVM_RECEIVER; documented in .env.example.
  • Delegation cache now has TTL + re-mint + failure-invalidation + bounded maps + $5 demo limit.
  • Dropped the deprecated environment option (agent + register-plans).
  • cityOf skips leading question words; deploy-README MPP-guard attribution corrected.

One item only partially in this repo's control: the viewer key arrives as a query param from the App's /auth/cli flow (→ access logs). Added forward-compat fragment reading + a scrub/App-fragment note in the deploy README; the clean fix is App-side. Re-requesting review.

@aaitor
aaitor requested a review from r-marques September 5, 2026 10:08

@r-marques r-marques left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 Fixedsrc/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, head fb39059a.
  • 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 client reference anywhere in http-simple-agent-ts or showcase. The hits in http-simple-agent-py/** and showcase/content/tutorials.ts:250 are the Python tutorial's own client.py and are correctly untouched; the repo-root CLAUDE.md mentions yarn client only as a generic "most tutorials" example, which is still true of the Python one, so I am not flagging it.
  • Confirmed Dockerfile ENTRYPOINT ["yarn","start"]node dist/agent.js matches the start script, and that tsconfig (include: src/**/*) deliberately leaves scripts/ to tsx.

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 environment option, 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

Comment thread http-simple-agent-ts/scripts/smoke.ts Outdated
Comment thread showcase/lib/live-agent.mjs Outdated
Comment thread http-simple-agent-ts/deploy/argocd/README.md Outdated

@r-marques r-marques left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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).
@aaitor
aaitor requested a review from r-marques September 5, 2026 10:23
@aaitor
aaitor merged commit cac7931 into main Sep 5, 2026
4 checks passed
@aaitor
aaitor deleted the aaitor/x402_mpp_tutorials branch September 5, 2026 10:24
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.

2 participants