@parallel-protocol/mpp
v0.1.1
Published
Parallel Protocol MPP (Machine Payments Protocol) merchant middleware — the Payment HTTP auth-scheme dance (402 challenge → credential → receipt) over the facilitator's /mpp endpoints
Readme
@parallel-protocol/mpp
Merchant middleware for the MPP (Machine Payments Protocol) charge intent — the Payment HTTP auth-scheme dance, settled through the Parallel facilitator.
MPP is the sibling of @parallel-protocol/x402: same merchant ergonomics (a middleware in front of a paid route), same facilitator backend, different wire protocol. Where x402 uses a 402 response with a custom payment-required header, MPP follows the RFC 9110 WWW-Authenticate convention with a Payment auth-scheme.
How it works
Agent Merchant (this SDK) Facilitator
| | |
|── GET /api/quote ─────────────>| |
| | (no Authorization header) |
|<─ 402 + WWW-Authenticate ──────| |
| Payment id="...", | |
| method="evm", | |
| intent="charge", | |
| request="<base64url>" | |
| | |
| [agent decodes request, | |
| signs EIP-3009 authorization, | |
| builds credential JSON] | |
| | |
|── GET /api/quote ─────────────>| |
| Authorization: Payment | |
| <base64url(credential)> |── POST /mpp/verify ────────>|
| |<─ { valid: true } ──────────|
| | |
| | [handler runs] |
| | |
| |── POST /mpp/settle ────────>|
| |<─ MppReceipt ───────────────|
| | |
|<─ 200 + Payment-Receipt ───────| |
| <base64url(receipt)> | |Key properties:
- The handler only runs after the credential is verified.
- Settlement (on-chain) happens after a 2xx handler response — the agent is never charged on errors.
- If settlement fails, the middleware returns
402and discards the handler response. - CORS preflight (
OPTIONS) is always passed through without challenge. - The
402challenge body also includes thechallengeobject as JSON for agents that prefer parsing the body over parsing the header.
Install
npm install @parallel-protocol/mpp
# or
bun add @parallel-protocol/mppInstall your framework separately (all are optional peer dependencies):
npm install express # Express 4 or 5
npm install next # Next.js 14 or 15
npm install fastify # Fastify 4 or 5
npm install hono # Hono 4+Quick start
Hono
import { Hono } from "hono";
import { paymentMiddleware } from "@parallel-protocol/mpp/hono";
const app = new Hono();
app.use(
paymentMiddleware({
facilitator: { url: "https://agents.parallel.best" },
routes: {
"/api/quote": {
price: "0.10", // 0.10 USDC (6 decimals by default)
network: "base",
currency: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
payTo: "0xYourAddress",
method: "evm",
},
},
}),
);
app.get("/api/quote", (c) => c.json({ quote: 42 }));Express
import express from "express";
import { paymentMiddleware } from "@parallel-protocol/mpp/express";
const app = express();
app.use(
paymentMiddleware({
facilitator: {
url: "https://agents.parallel.best",
},
routes: {
"/api/data": {
price: "0.01",
decimals: 18, // USDp has 18 decimals
network: "ethereum",
currency: "0x9B3a8f7CEC208e247d97dEE13313690977e24459", // USDp on Ethereum
payTo: "0xYourAddress",
method: "parallel", // unlocks all routes A–L
},
},
}),
);
app.get("/api/data", (req, res) => {
res.json({ data: "protected content" });
});Next.js (App Router)
// app/api/data/route.ts
import { withPayment } from "@parallel-protocol/mpp/next";
export const GET = withPayment(
{
facilitator: { url: "https://agents.parallel.best" },
routes: {
"/api/data": {
price: "0.05",
decimals: 18, // USDp has 18 decimals
network: "base",
currency: "0x76A9A0062ec6712b99B4f63bD2b4270185759dd5", // USDp on Base
payTo: "0xYourAddress",
},
},
},
async (req) => {
return Response.json({ data: "protected content" });
},
);Fastify
import Fastify from "fastify";
import { paymentMiddleware } from "@parallel-protocol/mpp/fastify";
const app = Fastify();
await app.register(
paymentMiddleware({
facilitator: { url: "https://agents.parallel.best" },
routes: {
"/api/data": {
price: 1000000n, // 1 USDC as raw bigint (6 decimals → skip parsing)
network: "avalanche",
currency: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
payTo: "0xYourAddress",
},
},
}),
);
app.get("/api/data", async () => ({ data: "protected content" }));Configuration
MppMiddlewareConfig
interface MppMiddlewareConfig {
facilitator: MppFacilitatorConfig;
routes: Record<string, MppRouteConfig>;
}MppFacilitatorConfig
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| url | string | Yes | Base URL of the Parallel facilitator (e.g. https://agents.parallel.best). The SDK internally appends /mpp/verify and /mpp/settle to this value. |
| apiKey | string | No | Reserved for future use. If provided, sent as X-API-Key header on facilitator requests but currently not enforced server-side. |
MppRouteConfig
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| price | string \| bigint | Yes | Amount required. A decimal string like "0.10" is parsed using decimals. Pass a bigint to skip parsing. |
| decimals | number | No | Token decimals for string price parsing. Default: 6 (USDC). Use 18 for USDp or sUSDp. |
| network | string | Yes | Chain slug: "ethereum", "base", "avalanche", "hyperevm", etc. Must be in the Parallel chain catalog. |
| payTo | Address | Yes | Merchant receiving address. |
| currency | Address | Yes | ERC-20 token the payer authorizes. One token per route. |
| method | "evm" \| "parallel" | No | Default: "evm". Controls the credential shape agents must provide (see Methods). |
| maxTimeoutSeconds | number | No | Challenge validity window in seconds. Default 300. Informational only — the nonce TTL is enforced by the facilitator. |
| description | string | No | Human-readable description included in the challenge. |
| realm | string | No | Optional realm identifier in the WWW-Authenticate header. |
Route matching uses longest prefix wins: /api/v1/data matches /api/v1 before /api.
Note: Unlike x402's
acceptedTokenslist, MPP charges a single currency per route. To accept multiple tokens, declare multiple routes.
Methods
The method field controls what the agent must sign:
"evm" (default) — Route A only
Standard MPP evm/authorization. The agent provides a flat EIP-3009 authorization:
// credential.payload shape for method="evm"
{
type: "authorization",
from: "0xAgentAddress",
to: "0xMerchantAddress",
value: "100000", // in token base units (e.g. 0.10 USDC = 100000 with 6 decimals)
validAfter: "0",
validBefore: "1750000000", // Unix timestamp
nonce: "0xabc...", // 32-byte random
signature: "0xabc...", // EIP-712 signature over TransferWithAuthorization
}Settles as Route A: a direct transferWithAuthorization on-chain. Maximum compatibility — payable by any MPP evm client.
"parallel" — Routes A–L
Parallel's extension method. The credential payload is a full SignedPaymentPayload (same wire format as x402). This unlocks all 12 facilitator routes (A–L), including:
- Routes A / F / J: direct transfers (USDp→USDp, USDC→USDC, sUSDp→sUSDp)
- Routes B / D: USDp → collateral swap via Parallelizer (exact output)
- Routes E / G: USDC → USDp or sUSDp swap via Parallelizer (exact input)
- Route C: USDp deposit into sUSDp savings vault
- Routes H / I / K: sUSDp redemption (→ USDp, USDC, or other collateral)
- Route L: partial sUSDp redeem combined with USDp
See @parallel-protocol/payment-core for signing helpers and @parallel-protocol/x402 for a full route table.
Wire protocol
402 challenge response
When no valid Authorization: Payment header is present:
HTTP/1.1 402 Payment Required
Content-Type: application/json
WWW-Authenticate: Payment id="a1b2c3d4", method="evm", intent="charge", request="eyJhbW91bnQiOiIxMDAwMDAiLCJjdXJyZW5jeSI6IjB4ODMzNS4uLiIsInJlY2lwaWVudCI6IjB4TWVyY2hhbnQiLCJtZXRob2REZXRhaWxzIjp7ImNoYWluSWQiOjg0NTN9fQ", expires="2025-01-15T12:05:00.000Z", description="AI model inference"The request param is base64url(JSON<EvmChargeRequest>):
// Decoded EvmChargeRequest
{
amount: "100000", // bigint as string, in token base units
currency: "0x8335...", // token address
recipient: "0xMerchant", // merchant's payTo
description?: "...",
methodDetails: { chainId: 8453 }
}The body is JSON and also includes the challenge object for agents that parse the body:
{
"challenge": {
"id": "a1b2c3d4",
"method": "evm",
"intent": "charge",
"request": "<base64url>",
"expires": "2025-01-15T12:05:00.000Z"
}
}On re-challenge (verification failed), the body also includes { "error": "INVALID_SIGNATURE" }.
Request: Authorization: Payment
The agent encodes the full MppChargeCredential as base64url(JSON(...)) and passes it in the Authorization header:
Authorization: Payment eyJjaGFsbGVuZ2UiOnsiLi4uIn0sInBheWxvYWQiOnsidHlwZSI6ImF1dGhvcml6YXRpb24iLC4uLn19The credential JSON structure:
interface MppChargeCredential {
challenge: MppChallenge; // Echo of the server's challenge
source?: string; // Payer DID (optional, org.paymentauth)
payload: EvmAuthorizationPayload // method="evm"
| SignedPaymentPayload; // method="parallel"
}200 response: Payment-Receipt
On success, the middleware sets a Payment-Receipt header with base64url(JSON<MppReceipt>):
interface MppReceipt {
status: "success";
method: "evm" | "parallel";
timestamp: string; // RFC 3339
reference: string; // on-chain txHash
}Exports
Main entry (@parallel-protocol/mpp)
import {
// Middleware (framework-agnostic)
createMppPaymentGate,
createMppPaymentMiddleware,
// Facilitator client
MppFacilitatorClient,
// Challenge codec
buildChallenge,
serializeChallenge,
parseAuthorizationHeader,
type MppChallenge,
// Errors
MppConfigError,
MppRuntimeError,
MPP_ERROR_CODES,
type MppErrorCode,
// Types
type MppFacilitatorConfig,
type MppMiddlewareConfig,
type MppRouteConfig,
type MppSettleResult,
type MppSettleSuccess,
type MppSettleFailure,
type PaymentGateResult,
type HTTPAdapter,
// Wire helpers (re-exported from mpp-types)
encodeBase64Url,
decodeBase64Url,
type MppChargeCredential,
type MppReceipt,
// Utils
chainIdFromSlug,
matchRoute,
parsePrice,
} from "@parallel-protocol/mpp";Framework adapters
| Import path | Export | Framework |
|-------------|--------|-----------|
| @parallel-protocol/mpp/express | paymentMiddleware(config) | Express 4/5 |
| @parallel-protocol/mpp/next | withPayment(config, handler) | Next.js 14/15 |
| @parallel-protocol/mpp/fastify | paymentMiddleware(config) | Fastify 4/5 |
| @parallel-protocol/mpp/hono | paymentMiddleware(config) | Hono 4+ |
Advanced: createMppPaymentGate
For custom integrations or frameworks not listed above:
import { createMppPaymentGate } from "@parallel-protocol/mpp";
const gate = createMppPaymentGate(config);
const result = await gate({
getHeader: (name) => request.headers[name],
getMethod: () => request.method,
getPath: () => request.path,
getUrl: () => request.url,
});
if (result.type === "pass") {
// Route not configured, or OPTIONS — proceed normally
} else if (result.type === "error") {
// 402 challenge: result.result.status / .headers / .body
} else {
// result.type === "verified"
// Run your handler...
const handlerStatus = await runHandler();
if (handlerStatus >= 200 && handlerStatus < 300) {
const settlement = await result.settle();
if (settlement.success) {
// settlement.receipt.reference = txHash
}
}
}Challenge codec (server utilities)
buildChallenge(route)
Builds an MppChallenge for a route. Generates a random id, encodes the charge request as base64url, and computes expires from maxTimeoutSeconds.
import { buildChallenge, serializeChallenge } from "@parallel-protocol/mpp";
const challenge = buildChallenge(routeConfig);
// { id: "uuid-v4", method: "evm", intent: "charge", request: "base64url...", expires: "..." }
const headerValue = serializeChallenge(challenge);
// 'Payment id="...", method="evm", intent="charge", request="...", expires="..."'serializeChallenge(challenge)
Serializes a challenge to a WWW-Authenticate: Payment … header value (RFC 9110 auth-params format). Undefined fields are omitted automatically.
parseAuthorizationHeader(header)
Parses an Authorization: Payment <token> header into a validated MppChargeCredential. Returns null when the header is absent, not the Payment scheme, the base64url token is malformed, or the JSON fails schema validation.
import { parseAuthorizationHeader } from "@parallel-protocol/mpp";
const credential = parseAuthorizationHeader(req.headers.authorization);
if (!credential) {
// Issue 402 challenge
}MppFacilitatorClient
Low-level client for the facilitator's MPP endpoints:
import { MppFacilitatorClient } from "@parallel-protocol/mpp";
const client = new MppFacilitatorClient({
url: "https://agents.parallel.best",
});
// Phase 1: verify signature + reserve nonce (no on-chain tx)
await client.verify(credential); // throws MppRuntimeError on failure
// Phase 2: submit on-chain, return receipt
const result = await client.settle(credential);
if (result.success) {
console.log(result.receipt.reference); // txHash
} else {
console.error(result.error.code); // e.g. "SUBMISSION_FAILED"
}Both methods use a 10-second timeout. verify throws on any failure; settle returns a MppSettleResult union on facilitator errors and throws MppRuntimeError only on connectivity issues.
Error codes
Thrown by the SDK
| Error class | When |
|-------------|------|
| MppConfigError | Invalid config at startup (bad payTo, bad currency, unknown network, invalid facilitator URL) |
| MppRuntimeError | Runtime failure; .code is one of the values below |
| MppRuntimeError.code | Meaning |
|------------------------|---------|
| FACILITATOR_UNAVAILABLE | Facilitator timed out (10s) or returned a non-JSON error |
| FACILITATOR_INVALID_RESPONSE | Facilitator returned an unexpected response shape |
| INVALID_CREDENTIAL | Credential could not be parsed or failed schema validation |
| METHOD_MISMATCH | Credential challenge.method doesn't match the route's configured method |
Propagated from the facilitator
When the facilitator rejects a credential, its error code is forwarded as the error field in the re-issued 402 body:
| Code | Meaning |
|------|---------|
| INVALID_SIGNATURE | EIP-712 signature recovery failed |
| INVALID_NONCE | Nonce already used (replay detected) |
| PAYMENT_EXPIRED | validBefore has passed |
| INSUFFICIENT_AMOUNT | Signed amount < required amount |
| NETWORK_MISMATCH | Payload chain ≠ route config chain |
| RATE_LIMIT_EXCEEDED | Signer exceeded the hourly transaction limit |
| PARALLELIZER_PAUSED | On-chain router temporarily paused (swap routes only) |
Utilities
import { chainIdFromSlug, matchRoute, parsePrice } from "@parallel-protocol/mpp";| Function | Signature | Description |
|----------|-----------|-------------|
| parsePrice | (price: string \| bigint, decimals?: number) => bigint | Parse "0.10" → 100000n (6 decimals by default) |
| chainIdFromSlug | (network: string) => number \| undefined | "base" → 8453, undefined for unknown chains |
| matchRoute | <T>(path: string, routes: Record<string, T>) => T \| undefined | Longest-prefix route matching |
Comparison with x402
| | @parallel-protocol/x402 | @parallel-protocol/mpp |
|-|--------------------------|--------------------------|
| Challenge status | 402 | 402 |
| Challenge header | payment-required: <base64> | WWW-Authenticate: Payment … |
| Credential header | payment-signature: <base64> | Authorization: Payment <base64url> |
| Receipt header | payment-response: <base64> | Payment-Receipt: <base64url> |
| Token per route | list (acceptedTokens) | single (currency) |
| Default decimals | 18 | 6 |
| Auth standard | x402 / Coinbase | RFC 9110 Payment auth-scheme |
| Facilitator endpoints | /x402/verify, /x402/settle | /mpp/verify, /mpp/settle |
| On-chain routes | A–L | A–L (same) |
| Cashback | Yes | Yes (same) |
Both protocols share the same facilitator backend, the same SignedPaymentPayload authorization format, and the same settlement routes.
Related packages
| Package | Purpose |
|---------|---------|
| @parallel-protocol/mpp-types | Wire-format Zod schemas + mppCredentialToPayment() helper |
| @parallel-protocol/x402 | HTTP-402 payment middleware (x402 / Coinbase protocol) |
| @parallel-protocol/payment-core | EIP-3009 signing helpers shared by x402 and MPP |
| @parallel-protocol/chains | 25-chain catalog with contract addresses |
License
MIT © Parallel Protocol
