# Machine Payments Protocol (MPP) Router — Integration Guide

## Why MPP Router

88 API services (OpenAI, Anthropic, OpenRouter, fal.ai, Perplexity, Stability AI, Replicate, Suno, Grok, and more) accept MPP payments — but only on Base and Tempo, which locks Stellar-native agents out. MPP Router bridges the gap: pay USDC once on Stellar and the router settles with the merchant in under 5 seconds, with no bridging, no gas, and no extra wallet. The router's catalog mirrors mpp.dev — currently around 489 paid POST endpoints across 88 services, all reachable from a single Stellar wallet.

## Supported wallets

- **Stellar wallets** — any Stellar USDC wallet works, including C Wallet and any client built on the Stellar MPP Client SDK (`@stellar/mpp/charge/client` for legacy mppx flow, or `@x402/stellar/exact/client` for the standard x402 v2 flow).
- **Base / Tempo wallets (and other schemes)** — forward directly to the upstream merchant as a transparent proxy

## Supported payment flavors

The router accepts two inbound payment protocols on the same public URLs:

1. **Stellar MPP (mppx)** — the original flavor, used by `@stellar/mpp/charge/client`. Send `Authorization: Payment <base64>` after receiving the router's `WWW-Authenticate` 402 challenge.
2. **Stellar x402 (v2)** — the open standard from x402.org, used by `@x402/stellar/exact/client` and any spec-compliant x402 client. Send `Payment-Signature: <base64>` after receiving the router's `Payment-Required` 402 challenge.

Both flavors are served from the same `/v1/services/...` URLs. The router emits a **dual-format 402** on the no-credential probe — both `WWW-Authenticate` (mppx) and `Payment-Required` (x402) headers in the same response — so each client reads the format it understands without any router-specific code. See the "Vanilla x402 client" example below.

## Discover Services

The router's `/services` endpoint returns the live catalog. As of 2026-04-12 the catalog is generated from a frozen snapshot of mpp.dev's 88-service registry; new services are added by re-running `scripts/admin/refresh-mpp-snapshot.ts` on the router side. Each entry includes the route public path, price, categories, payment methods, and (where the operator has tested it) a `verified_mode` flag.


## Overview

MPP Router (Machine Payments Protocol Router) provides a single public API for accessing supported paid services. Stellar agents use the router as a Stellar→Tempo payment proxy; agents with other wallet types (EVM, Bearer tokens, Basic auth, etc.) use it as a transparent HTTP proxy.

Public base URL:

```text
https://apiserver.mpprouter.dev
```

Clients must integrate only with MPP Router public URLs. Internal upstream providers, routing logic, and settlement details are not part of the public contract.

## Who the router represents

The router only **represents** Stellar wallets on the payment side. Concretely:

- **Stellar agents** (send an `Authorization: Payment ... method="stellar" ...` credential, or no `Authorization` at all) — the router verifies the credential against its mppx handler and, on success, pays the upstream merchant from its Tempo pool on the agent's behalf. The agent only ever needs USDC on Stellar.
- **Non-Stellar agents** (Bearer, Basic, EVM x402 / Tempo credentials, Stripe credentials, or any other `Authorization` scheme) — the router forwards the entire request including the `Authorization` header to the upstream merchant **as-is**. The router does not spend from its Tempo pool on your behalf, does not attach a `Payment-Receipt` header, and does not participate in replay protection for your credential. Whatever payment or auth contract exists is between you and the merchant.

You still get the benefit of a single hostname and unified service discovery regardless of wallet type.

## Public API Contract

All client requests must be sent to Router-managed URLs under:

```text
https://apiserver.mpprouter.dev
```

Public routes use stable aliases:

```text
/v1/services/{service}/{operation}
```

Examples (operator-verified end-to-end):

- `POST /v1/services/parallel/search` — general web search ($0.10)
- `POST /v1/services/exa/search` — AI web search
- `POST /v1/services/firecrawl/scrape` — web scraping
- `POST /v1/services/openrouter/chat` — chat completions (dynamic price, session mode)
- `POST /v1/services/openai/chat` — OpenAI chat completions (session mode)
- `POST /v1/services/gemini/generate` — Google Gemini text (session mode, `?model=gemini-2.0-flash`)
- `POST /v1/services/alchemy/rpc` — Ethereum mainnet RPC
- `POST /v1/services/tempo/rpc` — Tempo L2 RPC
- `POST /v1/services/storage/upload` — object storage upload

Image / video generation (verified end-to-end via fal.ai → Tempo → Stellar):

- `POST /v1/services/fal/flux_schnell` — FLUX.1 [schnell] text-to-image ($0.003)
- `POST /v1/services/fal/flux_dev` — FLUX.1 [dev] high-quality text-to-image ($0.025)
- `POST /v1/services/fal/flux-pro_v1_1` — FLUX1.1 [pro] ($0.035)
- `POST /v1/services/fal/recraft-v3` — Recraft V3 vector art ($0.040)
- `POST /v1/services/fal/stable-diffusion-v35-large` — SD 3.5 Large ($0.035)

Plus ~480 more endpoints across 88 services. Always call `GET /services` for the live list.

## Service Discovery

### `GET /health`

Returns router health and basic public status.

### `GET /services`

Returns the public service catalog. As of 2026-04-12 the catalog contains ~489 paid POST endpoints across 88 services. Top-level shape:

```json
{
  "version": 1,
  "base_url": "https://apiserver.mpprouter.dev",
  "supported_payment_methods": [
    { "scheme": "stellar.mpp",  "network": "stellar:pubnet" },
    { "scheme": "stellar.x402", "network": "stellar:pubnet" }
  ],
  "services": [ /* ~489 entries */ ]
}
```

Each entry exposes:

```json
{
  "id": "fal_flux_schnell",
  "name": "fal.ai – FLUX.1 [schnell] - Fast text-to-image (1-4 steps)",
  "category": "ai",
  "categories": ["ai", "media"],
  "description": "FLUX.1 [schnell] - Fast text-to-image (1-4 steps)",
  "public_path": "/v1/services/fal/flux_schnell",
  "method": "POST",
  "price": "$0.003/request",
  "payment_method": "stellar",
  "network": "stellar-mainnet",
  "asset": "USDC",
  "status": "active",
  "docs_url": "https://apiserver.mpprouter.dev/docs/integration#fal-flux-schnell",
  "methods": {
    "stellar":      { "intents": ["charge", "channel"] },
    "stellar_x402": { "scheme": "exact", "network": "stellar:pubnet", "pay_to": "G...", "asset": "USDC" },
    "tempo":        { "intents": ["charge"], "role": "upstream" }
  },
  "verified_mode": "charge"
}
```

The legacy `category` (singular) is preserved for v1 clients; new clients should read the `categories` array. `methods.stellar_x402` is present only when the router has its x402 inbound branch enabled.

### `GET /v1/services/catalog`

Returns the same public catalog under a versioned path.

### `GET /x402/supported`

Returns a standard x402 `SupportedResponse`-shaped JSON listing the schemes and networks the router accepts. Spec-compliant x402 clients can read this for zero-config discovery instead of parsing `/services`.

The public catalog contains only Router-facing metadata. It does not expose internal upstream domains or routing preferences.

## Payment Model — Stellar agents (mppx, legacy)

When a Stellar agent calls a paid route without a credential, the router returns:

- HTTP `402 Payment Required`
- a `WWW-Authenticate` header carrying a Stellar MPP payment challenge
- a `Payment-Required` header carrying the same quote in standard x402 v2 format (so x402 clients on the same URL can read it)
- a JSON body describing the challenge for human-friendly debugging

The mppx-format challenge is issued by the router itself (`realm="apiserver.mpprouter.dev"`) and is HMAC-bound to the exact amount, currency (USDC SAC contract), and recipient (router pool address) that the agent must pay. Clients pay the **router's** Stellar address, not the internal upstream provider.

## Payment Model — Stellar agents (x402, recommended for new clients)

The router also implements the x402 v2 protocol for inbound payments. Any client built on `@x402/core/client` (with `@x402/stellar/exact/client` registered as the scheme) can pay the router with zero router-specific code:

1. Client POSTs to a route with no auth header.
2. Router returns 402 with a `Payment-Required` header (base64 JSON of an x402 `PaymentRequired` object).
3. Client calls `x402HTTPClient.getPaymentRequiredResponse(getHeader)` to decode it.
4. Client calls `x402HTTPClient.createPaymentPayload(paymentRequired)` to sign a Soroban auth entry.
5. Client retries with `Payment-Signature: <base64>` header.
6. Router enters its `stellar.x402` dispatch branch, runs `@x402/stellar`'s facilitator verify (Soroban simulation + auth-entry validation), pays the downstream merchant via Tempo, and on a successful merchant 2xx submits the agent's signed Soroban invoke on chain.
7. Router returns the merchant body plus `X-Payment-Tx`, `X-Payment-Method`, and `X-Payment-Settle-Status` receipt headers.

The x402 inbound branch uses a separate router-side recipient address (`STELLAR_X402_PAY_TO`) from the legacy mppx pool. Both branches share the same Tempo pool for paying upstream merchants. Replay protection is per-payload (SHA-256 of the signed Soroban tx XDR) in router-side KV, with on-chain Soroban auth-nonce as the second line of defense.

### Replay protection and credential reuse

Each Stellar credential is **single-use**. After the first successful verification, the router stores the challenge id (and, for push-mode submissions, the on-chain transaction hash) in its deduplication store. A second request reusing the same `Authorization` header is rejected with `402` and a `challenge_already_used` error — do not retry with the same credential; get a fresh challenge instead.

The router also validates that the credential's echoed challenge matches the live merchant quote on the exact amount, currency, and recipient. You cannot reuse a credential obtained for a cheap route on an expensive one.

### Expected Stellar client flow

1. Client sends a request to `https://apiserver.mpprouter.dev/v1/services/{service}/{operation}`.
2. Router probes the upstream merchant to get a live price and returns `402 Payment Required` with a Stellar MPP challenge.
3. Client signs a USDC Soroban SAC `transfer` transaction (or submits one and captures its hash) using the Stellar MPP client SDK.
4. Client retries the same request with `Authorization: Payment <credential>`.
5. Router verifies the credential, pays the upstream merchant on Tempo, and returns the merchant's content along with a `Payment-Receipt` header containing the Stellar transaction hash.

### Fee sponsorship

The router runs a Stellar gas sponsor account that pays network fees on behalf of agents that do not hold XLM. In this mode the agent signs the Soroban authorization entries but leaves the transaction source as the all-zeros placeholder; the router rebuilds the envelope with its own source account, signs it, and submits. This is the default for fee-sponsored routes and is indicated by `methodDetails.feePayer: true` in the 402 challenge.

Push-mode submission (agent submits the transaction itself and sends the tx hash) is also supported, but **not** allowed in combination with fee sponsorship.

## Passthrough Model — non-Stellar agents

Send your request with whatever `Authorization` header the merchant expects. The router recognises any non-Stellar MPP scheme as a signal to transparently forward the request:

- `Authorization: Bearer <token>`
- `Authorization: Basic <base64>`
- `Authorization: Payment ... method="tempo" ...` (EVM x402 / Tempo credential)
- `Authorization: Payment ... method="stripe" ...`
- Any other scheme the router does not recognise as Stellar MPP

In passthrough mode:

- The request body, query string, and `Authorization` header reach the merchant untouched.
- The router makes a single outbound call (no probe-then-pay round-trip).
- Any `402`, `4xx`, or `5xx` response from the merchant is returned verbatim.
- The router does not attach a `Payment-Receipt` header — there is no router-issued receipt to attach.
- The router does not track or deduplicate your credential. Replay protection is the merchant's responsibility.

To opt into the Stellar flow from a client that might accidentally set some other `Authorization` header (e.g. an SDK default), simply omit the `Authorization` header on the first request. The router will then return a Stellar 402 challenge.

## Example — Stellar agent (mppx, legacy)

```ts
import { Mppx } from 'mppx/client'
import { stellar } from '@stellar/mpp/charge/client'
import { Keypair } from '@stellar/stellar-sdk'

const keypair = Keypair.fromSecret(process.env.STELLAR_SECRET!)

const mppx = Mppx.create({
  methods: [stellar.charge({ keypair })],
  polyfill: false,
})

const response = await mppx.fetch(
  'https://apiserver.mpprouter.dev/v1/services/parallel/search',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query: 'stellar payments' }),
  },
)

const data = await response.json()
console.log(data)
```

## Example — Stellar agent (vanilla x402 v2)

This example uses the `@x402/core/client` + `@x402/stellar/exact/client` combination from x402.org. There is **no** router-specific code — the same client can be pointed at any x402 v2 server. The router's dual-format 402 means the client reads `Payment-Required` automatically, signs a Soroban auth entry, and retries with `Payment-Signature`.

```ts
import { x402Client, x402HTTPClient } from '@x402/core/client'
import { createEd25519Signer } from '@x402/stellar'
import { ExactStellarScheme } from '@x402/stellar/exact/client'

const signer = createEd25519Signer(process.env.STELLAR_SECRET!, 'stellar:pubnet')
const coreClient = new x402Client().register(
  'stellar:*',
  new ExactStellarScheme(signer, { url: 'https://soroban-rpc.mainnet.stellar.gateway.fm' }),
)
const client = new x402HTTPClient(coreClient)

const url = 'https://apiserver.mpprouter.dev/v1/services/fal/flux_schnell'
const body = JSON.stringify({
  prompt: 'a flying pig with golden wings, photorealistic, cinematic lighting, 4k',
  image_size: 'square',
  num_inference_steps: 4,
})

// 1. Probe — no auth header. Router emits dual-format 402.
const probe = await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body,
})

// 2. Decode the standard x402 PaymentRequired from the response header.
const paymentRequired = client.getPaymentRequiredResponse(
  (name) => probe.headers.get(name),
  undefined,
)

// 3. Sign a Soroban auth entry.
const paymentPayload = await client.createPaymentPayload(paymentRequired)
const paymentHeaders = client.encodePaymentSignatureHeader(paymentPayload)

// 4. Retry with Payment-Signature header.
const paid = await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', ...paymentHeaders },
  body,
})

const result = await paid.json()
console.log(result.images[0].url)         // ← image URL from fal.ai
console.log(paid.headers.get('x-payment-tx'))  // ← Stellar settle tx hash
```

Verified end-to-end on Stellar mainnet 2026-04-12 (Stellar Expert: tx `84d52b919f4421e3c8c371f8e3a7c09ab4925ec2166d53067f379ced72f09b87`).

## Example — non-Stellar agent (passthrough)

```ts
const response = await fetch(
  'https://apiserver.mpprouter.dev/v1/services/parallel/search',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      // Any non-Stellar credential — forwarded untouched to the merchant.
      Authorization: 'Bearer my-merchant-issued-token',
    },
    body: JSON.stringify({ query: 'stellar payments' }),
  },
)

const data = await response.json()
console.log(data)
```

## Async Jobs (Signed Polling)

Some upstream services (e.g. StableStudio image/video generation, Seedance) are asynchronous — the paid request returns a `jobId` and the real result is fetched later. MPP Router handles the upstream SIWX/SIWE auth internally using its Tempo EVM wallet so Stellar agents can poll normally. Ownership of a pending job is cryptographically bound to the Stellar address that paid, using a short challenge-response flow.

### How it works

1. **Initial request**: `POST /v1/services/<service>/<op>` as usual with a Stellar credential. Pay once.
2. **202 response**: If the upstream is async, the router returns **`202 Accepted`** with:
   - `X-Job-Poll-Url` — the full URL to poll (`.../jobs/<jobId>`)
   - `X-Job-Id` — the job identifier
   - Body is the upstream's response (`{ "jobId": "...", "status": "queued" | "pending" | ... }`)

   The router also writes a `jobAuth` record keyed by `<jobId>` and bound to your Stellar G address. The record survives for 24 hours. This works even when the upstream returns `200` with a pending-status body (some providers like Nano-Banana-Pro don't use 202).
3. **Request a challenge**: ownership is verified per-poll via a one-time nonce.
   ```
   GET /v1/services/<service>/jobs/<jobId>/challenge
   X-Stellar-Owner: G...
   ```
   → `{ "nonce": "<64-hex-chars>", "expiresAt": "..." }`  (TTL 2 minutes; single-use)
4. **Sign the nonce** with your Stellar secret key. The Ed25519 signature is over the **hex-decoded** nonce bytes (32 bytes), not over the hex string.
5. **Poll for results**:
   ```
   GET /v1/services/<service>/jobs/<jobId>
   X-Stellar-Owner:     G...
   X-Stellar-Nonce:     <hex from step 3>
   X-Stellar-Signature: <base64 ed25519 sig>
   ```
6. **Response**: the router proxies the upstream job status back:
   - `200` — upstream body (`status: "complete"` → `result.imageUrl` / `videoUrl`; `status: "processing"` / `"queued"` → keep polling)
   - `401` — nonce missing / expired / wrong signature (request a fresh challenge)
   - `403` — signer verified but does not own this job
   - `404` — unknown or expired jobId (>24h)
   - `502` — upstream failure

### Why challenge-response

Parsing an mppx credential to read its `source` field is not authentication — nothing verifies the signature. The previous `Authorization: Payment <...>`-only contract was replaced in 2026-04 so that knowing a victim's G address is not enough to read their job. The new flow signs a server-issued random nonce with the Stellar secret key; verification uses `Keypair.fromPublicKey(G).verify(nonce, sig)`.

### Key details

- **Polling is free** — the initial POST is the only paid call.
- **24-hour availability** — `jobAuth` records expire 24h after creation.
- **Nonces are single-use** — each successful verify deletes the nonce. You always request a fresh one per poll.
- **SIWX proxy** — the router signs the upstream `Sign-In-With-X` challenge with its own Tempo EVM wallet. Agents never touch SIWE.
- **Breaking change from earlier drafts**: the old `Authorization: Payment <...>`-only polling path no longer works. Three headers are now required: `X-Stellar-Owner`, `X-Stellar-Nonce`, `X-Stellar-Signature`.

### Example — polling an async job (Stellar SDK / TypeScript)

```ts
import { Keypair } from '@stellar/stellar-base'

const ROUTER = 'https://apiserver.mpprouter.dev'
const kp = Keypair.fromSecret(process.env.STELLAR_SECRET!) // S...
const owner = kp.publicKey()                                // G...

// 1. Submit the job (pays USDC via your normal mppx/x402 client)
const submitRes = await mppx.fetch(
  `${ROUTER}/v1/services/stablestudio/generate_nano-banana-pro_generate`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt: '...', imageSize: '2K', aspectRatio: '16:9' }),
  },
)
const jobId = submitRes.headers.get('x-job-id')!

// 2. Poll loop
async function pollOnce(): Promise<any> {
  // 2a. Request challenge
  const cRes = await fetch(`${ROUTER}/v1/services/stablestudio/jobs/${jobId}/challenge`, {
    headers: { 'X-Stellar-Owner': owner },
  })
  const { nonce } = await cRes.json() as { nonce: string }

  // 2b. Sign the hex-decoded nonce bytes
  const nonceBytes = Buffer.from(nonce, 'hex')
  const sig = kp.sign(nonceBytes).toString('base64')

  // 2c. Authenticated poll
  const pRes = await fetch(`${ROUTER}/v1/services/stablestudio/jobs/${jobId}`, {
    headers: {
      'X-Stellar-Owner':     owner,
      'X-Stellar-Nonce':     nonce,
      'X-Stellar-Signature': sig,
    },
  })
  return pRes.json()
}

let body
do {
  await new Promise(r => setTimeout(r, 3000))
  body = await pollOnce()
} while (body.status === 'queued' || body.status === 'pending' || body.status === 'processing')

console.log(body.result) // { imageUrl: "..." }
```

The same pattern works from Python, Go, Rust, iOS, and Android — any Stellar SDK that exposes `Keypair.fromSecret(S).sign(bytes)`.

## Idempotency

Send an `X-Request-Id` header on write operations to get at-most-once semantics. The router caches the first successful response for 24 hours keyed by the header; subsequent requests with the same id return the cached body with an added `X-Idempotent: true` header. Rotate the id whenever the request body changes — idempotency is keyed purely on the header, not on the body hash.

## Error Handling

Clients should handle at least:

- `200` success (sync result)
- `202` accepted (async job — see "Async Jobs" section above)
- `400` invalid request or unknown public service route
- `402` payment required, or Stellar credential rejected. On Stellar-flow rejections the body is `application/problem+json` with an error code such as `invalid_challenge`, `payment_expired`, `challenge_already_used`, or `malformed_credential`. Do **not** retry with the same credential on `challenge_already_used` — obtain a fresh challenge.
- `429` reserved for future per-agent rate limiting
- `500` router internal error; safe to retry with a fresh challenge
- `502` merchant refused payment or returned an error. The error body includes the upstream status and truncated detail.

## Stability Guarantees

MPP Router treats the following as public contract:

- base URL: `https://apiserver.mpprouter.dev`
- versioned public path format (`/v1/services/{service}/{operation}`)
- documented catalog fields, including the `categories` array (the legacy singular `category` is preserved for backward compat)
- Router-issued payment challenge flow on both supported flavors:
  - mppx: `WWW-Authenticate: Payment ...` + `Authorization: Payment ...` reply
  - x402 v2: `Payment-Required: <base64>` + `Payment-Signature: <base64>` reply
- Dual-format 402 — every paid 402 carries both formats so clients of either flavor work without router-specific code
- The `GET /x402/supported` discovery endpoint (`SupportedResponse` shape from `@x402/core`)

MPP Router does not guarantee stability for internal upstream provider details. Operators may refresh the catalog from mpp.dev at any time, which can add or rename routes; verified-mode flags and stable IDs for the historically-tested 12 routes (parallel_search, exa_search, firecrawl_scrape, openrouter_chat, anthropic_messages, openai_chat, gemini_generate, dune_execute, modal_exec, alchemy_rpc, tempo_rpc, storage_upload) are preserved across refreshes via an operator overlay.

## What Clients Must Not Rely On

Clients must not rely on:

- upstream provider domains
- internal merchant hostnames
- internal routing order
- unpublished internal IDs
- direct third-party service URLs

Only Router URLs under `https://apiserver.mpprouter.dev` are public integration endpoints.

## Credits

Powered by [ROZO.AI](https://rozo.ai).

Supported by:
- Supported by Stellar Community Fund (SCF)
- Supported by Base Grants
- Circle Alliance member
