Skip to content

feat(mpp): add the MPP seller tutorial (two accounts, end to end) - #66

Draft
eruizgar91 wants to merge 1 commit into
mainfrom
feat/mpp-seller-tutorial
Draft

feat(mpp): add the MPP seller tutorial (two accounts, end to end)#66
eruizgar91 wants to merge 1 commit into
mainfrom
feat/mpp-seller-tutorial

Conversation

@eruizgar91

Copy link
Copy Markdown
Member

mpp-examples/mpp-seller/ — the first MPP tutorial, and the first that needs two accounts.

An ordinary Express service becomes a paid agent by adding one middleware, is published to the Nevermined catalog, and is then bought and called by a different account over MPP: no MPP secret on the seller, no protocol code on the buyer.

Nothing is mocked. It publishes a real plan and a real agent and burns real credits. The plan is free, so no testnet USDC is needed.

What it runs

Four acts, in this order because each proves something the next one hides:

  1. The raw wire — plain fetch, the 402 with WWW-Authenticate: Payment, buildCredentialHeader, the Payment-Receipt.
  2. The same call through payments.mpp.fetch — the whole 402-retry cycle behind one call, which is what a buyer actually writes.
  3. The negatives — a replayed credential and a swapped body, both re-challenged rather than served.
  4. The ledger — the credits that actually burned.

Notes for reviewers

  • Two accounts are required, and the demo refuses to run when the two keys are identical — MPP is about one account buying from another.
  • The API base is derived from the key prefix via getEnvironmentFromApiKey (sandbox: maps to api.sandbox.nevermined.app). No environment variable selects it; NVM_API_BASE only overrides.
  • A live API key is refused outright, and so is a live API base. The two checks are deliberately separate: NVM_API_BASE can point somewhere the key prefix did not choose, so a key-prefix check alone would not hold. The tutorial publishes real rows and creates a real spend mandate ($50/1h, revoked at the end of every run).
  • captureRawBody is load-bearing under mpp: { bindBody: true } — re-serializing req.body does not reproduce the bytes the buyer signed over, so the digest never matches and every paid request is re-challenged. That is act 3's second negative.
  • selfcheck.mjs runs the guards offline — no network, no credentials, nothing written. 12 checks, all green.
  • Provisioning counts against the account's tier caps (10 plans, 20 agents), so .demo-state.json reuses the pair by default; FORCE_PROVISION=yes publishes a new one.

Verification

Run end to end against the sandbox API, reusing an existing plan/agent pair: selfcheck 12/12, 402 then credential then 200 with a receipt, payments.mpp.fetch reporting paid=true settled=true credentialsPresented=1 creditsPresented=1, replay and body-swap both 402, 2 credits burned. TRANSCRIPT.md is that run.

Two disclosures are in the transcript itself: it ran against api.sandbox.nevermined.dev (where the two demo accounts live — the base is derived from the key prefix either way), and one [DEPRECATED] line the SDK prints against itself was removed.

Also

Root README.md gains the tutorial as entry 5; CLAUDE.md gains the mpp-examples/ tree entry, an MPP row in the protocol table, and a short section on the three non-obvious things above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KJ695LwV6AkP44PnfGxybu

An ordinary Express service becomes a paid agent with one middleware, is
published to the Nevermined catalog, and is then bought and called by a
DIFFERENT account over MPP — no MPP secret on the seller, no protocol code
on the buyer.

Nothing is mocked: it publishes a real plan and a real agent and burns real
credits. The plan is free, so no testnet USDC is needed.

Four acts, in this order because each proves something the next one hides:
the raw 402/credential/receipt wire, the same call through
`payments.mpp.fetch`, the negatives (a replayed credential and a swapped
body, both refused), and the ledger.

Notes for reviewers:

- Two accounts are required and the demo refuses to run when the two keys
  are identical — MPP is about one account buying from another.
- The API base is derived from the key prefix via `getEnvironmentFromApiKey`,
  so no environment variable selects it; `NVM_API_BASE` only overrides.
- A live API key is refused outright, and so is a live API base. The two
  checks are separate because `NVM_API_BASE` can point somewhere the key
  did not choose.
- `captureRawBody` is load-bearing under `mpp: { bindBody: true }`:
  re-serializing `req.body` does not reproduce the bytes the buyer signed
  over, so the digest never matches.
- `selfcheck.mjs` runs the guards offline — no network, no credentials,
  nothing written.

TRANSCRIPT.md is a real run against the sandbox API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJ695LwV6AkP44PnfGxybu
@eruizgar91
eruizgar91 marked this pull request as draft September 4, 2026 10:55
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