From 94d4ce77c7115b3f3fe4353bdf9e376dc1d6ab8a Mon Sep 17 00:00:00 2001 From: kaankacar Date: Wed, 19 Aug 2026 20:43:16 +0000 Subject: [PATCH 1/2] Add the MPP discovery layer to the agentic-payments skill The MPP guide covered Charge and Session mode but never explained how a paying agent finds the API in the first place. Add a Discovery section covering the OpenAPI 3.1 document at /openapi.json, the x-payment-info offers, and the mppx discovery() call that serves it. Also state the authority rule: the discovery document is advisory and the runtime 402 Challenge is authoritative for price, token, network, expiry, and terms. Name MPPScan and the mpp.dev services directory as optional registrations, and note that a listing does not verify any client payment. Route readers there from the SKILL.md decision table. --- skills/agentic-payments/SKILL.md | 2 ++ skills/agentic-payments/mpp.md | 60 +++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/skills/agentic-payments/SKILL.md b/skills/agentic-payments/SKILL.md index d689a4f..cfadf07 100644 --- a/skills/agentic-payments/SKILL.md +++ b/skills/agentic-payments/SKILL.md @@ -23,6 +23,7 @@ Two complementary protocols for AI-agent and machine-to-machine payments on Stel - Calling an x402 API from an agent → **x402 Buyer** in [x402.md](x402.md) - Selling an API, no facilitator dependency → **Charge mode** in [mpp.md](mpp.md) - Agent making many requests per session → **Session mode** in [mpp.md](mpp.md) +- Sold an API, now want agents to find it → **Discovery** in [mpp.md](mpp.md#discovery-let-agents-find-your-paid-api) - Unsure → x402 (lowest friction to get started) All protocols use USDC (SEP-41 SAC) by default; `stellar:testnet` / `stellar:pubnet` CAIP-2 network IDs. @@ -35,6 +36,7 @@ This file carries the decision table, the shared testnet account setup, and the |------|------| | Sell a paid API via a facilitator (zero-XLM clients), build an x402 buyer agent | [x402.md](x402.md) | | Facilitator-free per-request payments (Charge) or channel-backed sessions (Session) | [mpp.md](mpp.md) | +| Publish an OpenAPI discovery document so agents find your paid API | [Discovery](mpp.md#discovery-let-agents-find-your-paid-api) | | Create/fund testnet accounts, add USDC trustlines, get testnet USDC | [Testnet setup](#testnet-setup-shared) (below) | | Which USDC address goes where (classic issuer vs SAC) | [Two USDC addresses](#two-usdc-addresses-dont-confuse-them) (below) | diff --git a/skills/agentic-payments/mpp.md b/skills/agentic-payments/mpp.md index 9faec24..25439e3 100644 --- a/skills/agentic-payments/mpp.md +++ b/skills/agentic-payments/mpp.md @@ -217,6 +217,56 @@ console.log("Channel closed:", txHash); **Env vars (server):** `CHANNEL_CONTRACT`, `COMMITMENT_PUBKEY`, `MPP_SECRET_KEY`, `FEE_PAYER_SECRET` **Env vars (client):** `COMMITMENT_SECRET` +## Discovery: let agents find your paid API + +Charge and Session mode answer "how do I charge". Discovery answers "how does a paying agent find me". Without it you ship a working paid API that no agent can locate. + +A paid MPP server publishes an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) document at `GET /openapi.json`. Each paid operation carries an `x-payment-info` extension holding an `offers[]` array. Registries aggregate those documents so agents can search for paid APIs. + +> **Discovery is advisory.** The document is an informational hint for display and planning. The runtime **402 Challenge is authoritative** for price, token, network, expiry, and terms. Read the payment terms from the Challenge, never from the discovery document. + +### Serve the document + +`mppx/express` exports `discovery()`. It mounts `GET /openapi.json` and derives each offer from the method config and the per-route handler, so the document stays in step with the Challenges the route returns. + +```js +// charge-server.js (additions to the Charge mode server above) +import { Mppx, discovery } from "mppx/express"; + +// keep a reference to the route handler — discovery() reads the price from it +const pay = mppx.charge({ amount: "0.001", description: "paid API call" }); + +app.get("/data", pay, (req, res) => { + res.json({ result: "paid content", price: "$0.001 USDC" }); +}); + +discovery(app, mppx, { + info: { title: "Paid Data API", version: "1.0.0" }, + routes: [{ handler: pay, method: "get", path: "/data" }], +}); +``` + +Session mode works the same way — pass the `mppx.channel(...)` handler in `routes`. + +Validate the document before you publish it: + +```bash +npx mppx discover validate http://localhost:3002/openapi.json +``` + +### Register the service (optional) + +| Registry | What it is | How to list | +|----------|------------|-------------| +| [MPPScan](https://mppscan.com) | Public registry of MPP services, with search and analytics | [Register](https://www.mppscan.com/register) | +| [MPP services directory](https://mpp.dev/services) | Curated list of live services on mpp.dev | [Submission guide](https://mpp.dev/services#list-your-service) | + +Agents can query the curated directory over MCP at `https://mpp.dev/mcp/services`. That server is read-only. + +A registry listing advertises your service. It does not verify any client payment. Your server still issues the 402 Challenge and verifies the Credential on every request. + +Full reference: [MPP discovery docs](https://mpp.dev/advanced/discovery). + ## Packages and subpath imports ```bash @@ -230,7 +280,7 @@ npm install @stellar/mpp mppx @stellar/stellar-sdk | `@stellar/mpp/channel/server` | `import * as stellar from "@stellar/mpp/channel/server"` — use `stellar.channel(...)`, `stellar.close(...)`, `stellar.getChannelState(...)`, `stellar.watchChannel(...)` | | `@stellar/mpp/channel/client` | `import * as stellar from "@stellar/mpp/channel/client"` — use `stellar.channel(...)` | | `@stellar/mpp/channel` | Zod schema definitions for channel types | -| `mppx/express` | `import { Mppx } from "mppx/express"` — Express adapter; `Mppx.create(...)` returns per-route handlers | +| `mppx/express` | `import { Mppx, discovery } from "mppx/express"` — Express adapter; `Mppx.create(...)` returns per-route handlers, `discovery(...)` mounts `/openapi.json` | | `mppx/server` | `import { Mppx, Store } from "mppx/server"` — framework-agnostic server + `Store` | | `mppx/client` | `import { Mppx } from "mppx/client"` — client; also re-exported by `@stellar/mpp/charge/client` | @@ -277,6 +327,14 @@ npm install @stellar/mpp mppx @stellar/stellar-sdk - Symptom: `op_insufficient_balance` or fee errors on client-submitted transactions - Fix: set `mode: "pull"` on the client and configure `feePayer` on the server so the server pays fees. The client only signs auth entries. +**Discovery: client trusts the discovery price** +- Symptom: the client pays the amount from `/openapi.json` and the server rejects the Credential +- Fix: the discovery document is advisory. Take price, token, network, expiry, and terms from the 402 Challenge. + +**Discovery: route missing from `/openapi.json`** +- Symptom: the document builds, but a paid route carries no `x-payment-info` +- Fix: on Express, `discovery()` only documents routes listed in `routes`. Add one entry per paid route, and pass the same handler object you mounted on the route. + **`Store.memory()` in production** - Symptom: server loses track of channel state on restart, enables double-spend - Fix: replace `Store.memory()` with a persistent store (database-backed) before going to production. From bd743733e9c63a108e26c570903194b43da51289 Mon Sep 17 00:00:00 2001 From: Kaan Kacar Date: Wed, 19 Aug 2026 20:55:29 +0000 Subject: [PATCH 2/2] Make the discovery snippet an edit of the Charge mode server The block read as an append. Pasted that way it redeclares Mppx and registers a second /data route that never runs. Number the three edits instead, and tidy the intro. --- skills/agentic-payments/mpp.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/skills/agentic-payments/mpp.md b/skills/agentic-payments/mpp.md index 25439e3..19e2433 100644 --- a/skills/agentic-payments/mpp.md +++ b/skills/agentic-payments/mpp.md @@ -219,7 +219,7 @@ console.log("Channel closed:", txHash); ## Discovery: let agents find your paid API -Charge and Session mode answer "how do I charge". Discovery answers "how does a paying agent find me". Without it you ship a working paid API that no agent can locate. +Charge and Session modes answer one question: how do I charge? Discovery answers a second: how does a paying agent find me? Without discovery you ship a working paid API that no agent can locate. A paid MPP server publishes an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) document at `GET /openapi.json`. Each paid operation carries an `x-payment-info` extension holding an `offers[]` array. Registries aggregate those documents so agents can search for paid APIs. @@ -229,17 +229,23 @@ A paid MPP server publishes an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1. `mppx/express` exports `discovery()`. It mounts `GET /openapi.json` and derives each offer from the method config and the per-route handler, so the document stays in step with the Challenges the route returns. +Edit the Charge mode server above — don't append this to it. A second `mppx/express` import redeclares `Mppx`, and a second `/data` route never runs. + ```js -// charge-server.js (additions to the Charge mode server above) +// charge-server.js + +// 1. Replace the existing `mppx/express` import with this one. import { Mppx, discovery } from "mppx/express"; -// keep a reference to the route handler — discovery() reads the price from it +// 2. Replace the inline app.get("/data", ...) block with these two. +// discovery() reads the price off the handler object, so name it. const pay = mppx.charge({ amount: "0.001", description: "paid API call" }); app.get("/data", pay, (req, res) => { res.json({ result: "paid content", price: "$0.001 USDC" }); }); +// 3. Add this after the route, before app.listen(). discovery(app, mppx, { info: { title: "Paid Data API", version: "1.0.0" }, routes: [{ handler: pay, method: "get", path: "/data" }],