Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ tutorials/
├── langchain-langsmith-deployment-py/ # Route-level ASGI PaymentMiddleware (Python)
├── langchain-chat-ui-nvm/ # Next.js browser chat UI with the x402 card-delegation popup
├── catalog/ # Video showcase demos (song-from-the-headlines, diligence-in-a-box)
└── mcp-examples/ # Model Context Protocol (MCP) examples
├── weather-mcp/ # TypeScript MCP server (has CLAUDE.md)
└── weather-mcp-py/ # Python MCP server
├── mcp-examples/ # Model Context Protocol (MCP) examples
│ ├── weather-mcp/ # TypeScript MCP server (has CLAUDE.md)
│ └── weather-mcp-py/ # Python MCP server
└── mpp-examples/ # Machine Payments Protocol (MPP) examples
└── mpp-seller/ # Two-account seller/buyer run (JavaScript)
```

## Common Commands
Expand Down Expand Up @@ -65,6 +67,7 @@ PORT=3000
|----------|-------------|------------------|
| **x402** | HTTP 402 payment protocol | `http-simple-agent-ts/` (TS), `http-simple-agent-py/` (Python) |
| **MCP** | Model Context Protocol | `mcp-examples/` |
| **MPP** | Machine Payments Protocol — 402 challenge/credential over `WWW-Authenticate` | `mpp-examples/mpp-seller/` |

## x402 Protocol (v2)

Expand Down Expand Up @@ -124,6 +127,23 @@ injects the token into the run body, so pointing it at a different agent is a
- The per-call credit price is **not** an env var in the research/deep tutorials —
it is read from the plan's `registry.credits.maxAmount` at import time.

## MPP tutorial

`mpp-examples/mpp-seller/` is the only tutorial that needs **two** accounts, and it
refuses to run when the two API keys are identical — MPP is about one account buying from
another. Three things about it that are not obvious from the code:

- **The API base is derived from the key prefix** (`sandbox:` maps to
`api.sandbox.nevermined.app`), so no environment variable selects it. `NVM_API_BASE`
only overrides.
- **A live key is refused outright**, and so is a live API base. The tutorial publishes
real rows and creates a real spend mandate.
- **`captureRawBody` is load-bearing** whenever `mpp: { bindBody: true }` is set:
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.

Run `npm run selfcheck` (offline, no credentials) before touching the guards.

## Subdirectory CLAUDE.md Files

The following tutorials have their own CLAUDE.md with specific instructions:
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,36 @@ A freemium market-research agent on LangChain's [Deep Agents](https://docs.langc

---

### 5. MPP Seller — a paid agent bought by a second account (JavaScript)

**Location**: `mpp-examples/mpp-seller/`

A two-account, end-to-end run of the **Machine Payments Protocol (MPP)**: 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. Nothing is mocked — it
publishes a real plan and a real agent and burns real credits, on a free plan, so no
testnet USDC is needed.

**Technologies**:
- Node.js 20+ (plain ESM, no build step)
- Express
- Nevermined Payments SDK (`@nevermined-io/payments`, `paymentMiddleware`)

**What You'll Learn**:
- Protect a route with `paymentMiddleware` and `mpp: { bindBody: true }`
- Answer a `402` `WWW-Authenticate: Payment` challenge with a credential, by hand
- Do the same in one call with `payments.mpp.fetch`
- See why a credential is single-use and cannot be moved to another request body
- Tell your account address from your spend address, and know which one holds credits

**Features**:
- The seller holds no MPP secret — the middleware forwards opaque strings to the API
- Negative cases included: a replayed credential and a swapped body, both refused
- A spend-capped delegation, revoked at the end of every run
- Offline self-check (`npm run selfcheck`) and a recorded [transcript](./mpp-examples/mpp-seller/TRANSCRIPT.md)

---

## Quick Start

Each tutorial includes detailed instructions for:
Expand Down
37 changes: 37 additions & 0 deletions mpp-examples/mpp-seller/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# =============================================================================
# Two accounts are required — this tutorial is about ONE account buying from
# ANOTHER. Both keys must come from the same environment, and the demo refuses
# to run if they are identical.
#
# Get them at https://nevermined.app (Settings -> API keys). A `sandbox:` key
# costs nothing; a `live:` key is refused outright.
# =============================================================================

# Account A — publishes the plan and the agent, and runs the paid endpoint.
BUILDER_NVM_API_KEY=sandbox:your-builder-api-key

# Account B — buys the plan and calls the agent.
SUBSCRIBER_NVM_API_KEY=sandbox:your-subscriber-api-key

# =============================================================================
# Optional
# =============================================================================

# The API base is derived from the key prefix (`sandbox:` -> api.sandbox.nevermined.app).
# Set this only to point at a different deployment.
# NVM_API_BASE=https://api.sandbox.nevermined.app

# Port the agent listens on (default 8790).
# PORT=8790

# The endpoint registered in the catalog. Override it when the agent is not on
# localhost — the buyer calls exactly this URL.
# AGENT_URL=http://localhost:8790/ask

# Credits granted per plan purchase, and burned per request.
# CREDITS_GRANTED=20
# CREDITS=1

# The buyer's spend mandate: cap in cents, and how long it lives.
# DELEGATION_LIMIT_CENTS=5000
# DELEGATION_DURATION_SECS=3600
5 changes: 5 additions & 0 deletions mpp-examples/mpp-seller/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
.env
.env.local
.demo-state.json
*.log
228 changes: 228 additions & 0 deletions mpp-examples/mpp-seller/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# MPP Seller — a paid agent bought by a second account

A complete, two-account run of the **Machine Payments Protocol (MPP)**: 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 — with no
MPP secret on the seller and no protocol code on the buyer.

[![Discord](https://img.shields.io/badge/Discord-Join%20Us-7289da?logo=discord&logoColor=white)](https://discord.com/invite/GZju2qScKq)

Nothing here is mocked. It runs against the deployed sandbox API, publishes a real plan
and a real agent, and burns real credits. The plan is free, so no testnet USDC is needed.

## What You'll Learn

- Protect an Express route with `paymentMiddleware` and `mpp: { bindBody: true }`
- Read an MPP `402` challenge off `WWW-Authenticate` and answer it with a credential
- Do the same thing in one call with `payments.mpp.fetch`
- See why a credential is single-use, and why it cannot be moved to another request body
- Tell your account address from your spend address, and know which one holds the credits

## MPP payment flow

```
┌─────────┐ ┌─────────┐ ┌────────────┐
│ Buyer │ │ Seller │ │ Nevermined │
└────┬────┘ └────┬────┘ └─────┬──────┘
│ │ │
│ 1. POST /ask (no credential) │ │
│───────────────────────────────────────────>│ mint challenge │
│ │──────────────────────>│
│ 2. 402 + WWW-Authenticate: Payment id=… │ │
│<───────────────────────────────────────────│ │
│ │ │
│ 3. buildCredentialHeader(challenge, token) │
│ │ │
│ 4. POST /ask + Authorization: Payment … │ │
│───────────────────────────────────────────>│ verify + burn │
│ │──────────────────────>│
│ 5. 200 OK + Payment-Receipt │ │
│<───────────────────────────────────────────│ │
```

The seller never mints a challenge and never burns a credit itself: the middleware
forwards opaque strings to the Nevermined API, which does both. That is why a seller
needs no MPP secret of its own.

## Prerequisites

- Node.js 20+
- **Two Nevermined accounts** in the same environment, each with an API key
(https://nevermined.app → Settings → API keys). One publishes, one buys — the tutorial
refuses to run if the two keys are identical.

## Quick Start

```bash
# Install
npm install # or: yarn install

# Configure
cp .env.example .env # then fill in the two API keys

# Verify the offline guards (no network, no credentials, nothing written)
npm run selfcheck

# Run everything: provision (once) + serve + buy + tear down
./demo.sh
```

To publish a fresh plan/agent pair instead of reusing the one in `.demo-state.json`:

```bash
FORCE_PROVISION=yes ./demo.sh
```

Or drive the three steps yourself, in three terminals:

```bash
npm run provision # account A: register the plan + the agent
npm run agent # account A: serve the paid endpoint on :8790
npm run client # account B: buy the plan and call the agent
```

## Project Structure

| File | Who it is | What it does |
| --------------- | --------------------- | --------------------------------------------------------------------------- |
| `provision.mjs` | builder (account A) | Registers a free credits plan and a dummy agent; writes `.demo-state.json`. |
| `agent.mjs` | builder (account A) | The paid agent. `paymentMiddleware` with `mpp: { bindBody: true }`. |
| `client.mjs` | subscriber (account B)| Delegation, plan order, the MPP handshake, the negatives, the ledger. |
| `demo.sh` | — | Runs all three and prints the seller log. |
| `selfcheck.mjs` | — | The guards, offline. |

## How It Works

### 1. The seller — one middleware, no protocol code

```javascript
app.use(express.json({ verify: captureRawBody }))

app.use(
paymentMiddleware(payments, {
'POST /ask': {
planId: state.planId,
agentId: state.agentId,
credits: 1,
mpp: { bindBody: true },
},
}),
)
```

`bindBody: true` binds the challenge to a digest of the request body. `captureRawBody` is
required for it: re-serializing `req.body` would not reproduce the bytes the buyer sent,
so the digest would never match.

### 2. The buyer — the raw wire

```javascript
const unpaid = await fetch(url, { method: 'POST', headers, body }) // 402
const challenge = parseChallengeHeader(unpaid.headers.get('www-authenticate'))
const credential = buildCredentialHeader(challenge, { accessToken })
const paid = await fetch(url, { // 200
method: 'POST',
headers: { ...headers, authorization: credential },
body,
})
parseReceiptHeader(paid.headers.get('payment-receipt'))
```

### 3. The buyer — the same thing, in one call

```javascript
const result = await payments.mpp.fetch(
url,
{ method: 'POST', headers, body },
{ delegationConfig: { delegationId }, planId, maxCredits: 4 },
)
// result.paid / result.settled / result.credentialsPresented / result.creditsPresented
```

`payments.mpp.fetch` runs the whole 402-retry cycle behind one call. This is what a buyer
actually writes; act 1 exists only to show what it does.

## What the run proves

1. **The seller holds no MPP secret.** `agent.mjs` never mints a challenge.
2. **An unpaid request is challenged** — `402` with `WWW-Authenticate: Payment id="…"`.
3. **The credential pays** — `200` with a `Payment-Receipt` naming the challenge id.
4. **The buyer needs no protocol code** — `payments.mpp.fetch` reports
`paid / settled / credentialsPresented / creditsPresented`.
5. **A credential is single-use.** Replaying it is re-challenged, not served.
6. **The challenge is bound to the body.** The same credential with a different body is
refused — `bindBody: true` plus `captureRawBody` doing their job.
7. **The credits actually burn**, one per request, against the buyer's own ledger.

[`TRANSCRIPT.md`](./TRANSCRIPT.md) is a real, unedited run.

## Two things that will bite you

**You have two addresses, and only one of them holds credits.** Your API key's `sub` is
your **account** address: `plans.orderPlan` credits it, and every x402/MPP burn is charged
against it. The address inside a minted access token — `payerOf()` in `lib/config.mjs`
extracts it, act 4 prints it — is your **spend** account, the wallet that signs the
payment authorisation. It normally holds no credits at all. Read the balance for that one
and a run that burned credits reports `0 -> 0` while nothing looks wrong.

**`registerAgent` can lose a race with `registerPlan`.** The plan is written on-chain
asynchronously, so an agent created in the same breath is sometimes refused with
`BCK.PROTOCOL.0055` ("Payment plan does not exist on-chain"). It is a race, not a bad
request — `provision.mjs` retries that one code and nothing else.

## Environment Variables

| Variable | Required | Default | Purpose |
| -------------------------- | -------- | -------------------------- | ---------------------------------------------- |
| `BUILDER_NVM_API_KEY` | yes | — | Account A: publishes the plan and the agent. |
| `SUBSCRIBER_NVM_API_KEY` | yes | — | Account B: buys the plan and calls the agent. |
| `NVM_API_BASE` | no | derived from the key prefix| Point at a different deployment. |
| `PORT` | no | `8790` | Port the agent listens on. |
| `AGENT_URL` | no | `http://localhost:$PORT/ask` | The endpoint registered in the catalog. |
| `CREDITS_GRANTED` | no | `20` | Credits minted per plan purchase. |
| `CREDITS` | no | `1` | Credits burned per request. |
| `DELEGATION_LIMIT_CENTS` | no | `5000` | The buyer's spend cap. |
| `DELEGATION_DURATION_SECS` | no | `3600` | How long that mandate lives. |
| `FORCE_PROVISION` | no | — | `yes` publishes a fresh plan/agent pair. |
| `KEEP_DELEGATION` | no | — | `yes` keeps the delegation instead of revoking.|

The API base is **derived from the key prefix** — `sandbox:` resolves to
`api.sandbox.nevermined.app` — so nothing has to be configured by hand.

If you would rather keep the keys out of the project directory, put them in
`~/.nvm-mpp-demo.json` instead of `.env` (override the path with `DEMO_FILE`):

```bash
umask 077 && cat > ~/.nvm-mpp-demo.json <<'JSON'
{
"builderApiKey": "sandbox:…",
"subscriberApiKey": "sandbox:…"
}
JSON
```

## Safety

- **A live API key is refused outright**, whatever the deployment, and so is a live API
base. This tutorial publishes real rows and creates a real spend mandate; on `sandbox`
that costs nothing, on `live` it is real money.
- Provisioning writes real rows wherever it is pointed, and those count against the
account's tier caps (10 plans, 20 agents). `FORCE_PROVISION=yes` publishes a new pair
on **every** run — re-run without it to reuse the pair in `.demo-state.json`.
- The buyer's delegation is a live spend mandate ($50 for an hour by default) and is
revoked at the end of every run. `KEEP_DELEGATION=yes` keeps it.

## Troubleshooting

| Symptom | Cause |
| -------------------------------------------------- | ---------------------------------------------------------------------------- |
| `the builder and subscriber keys are identical` | Both keys are the same account. MPP needs a seller and a *different* buyer. |
| `BCK.PROTOCOL.0055` on provision | The plan is not on-chain yet. `provision.mjs` retries; it resolves in ~3–18s.|
| `no .demo-state.json` | Run `npm run provision` first — the agent reads the plan/agent ids from it. |
| A balance that never moves | You read the payer (spend) address; credits live on the account address. |
| `402` on **every** request, including the paid one | The body was re-serialized between challenge and call — `bindBody` digest. |

## Documentation

- [Nevermined Documentation](https://nevermined.ai/docs)
- [Nevermined Payments SDK](https://github.com/nevermined-io/payments)
Loading