Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
09bd3de
fix(sdk): unify discover on diagnose pipeline, validate payment headers
leventilo Mar 18, 2026
86bbbf8
fix(sdk): propagate abort signal, remove dead code, add negative tests
leventilo Mar 18, 2026
ed9e579
fix(sdk): adaptive health, tolerant parsing, redirect cycles
leventilo Mar 18, 2026
be567ca
feat(sdk): detect mpp protocol in diagnose pipeline
leventilo Mar 18, 2026
bb1c7ae
fix(sdk): harden mpp parsing against false positives and quoted values
leventilo Mar 18, 2026
6ab7a4e
feat(core): add mpp method quote and error types
leventilo Mar 19, 2026
c183f30
test(protocols): add mppx challenge credential request tests
leventilo Mar 19, 2026
90e71d0
feat(protocols): add mpp method selector and quote builder
leventilo Mar 19, 2026
84be3dc
feat(sdk): extend wallet schema and config for mpp wallets
leventilo Mar 19, 2026
8eda616
test(protocols): add mppx method receipt errors session tests
leventilo Mar 19, 2026
ad95692
fix(sdk): widen wallet type for mpp compatibility
leventilo Mar 19, 2026
972c1ba
feat(protocols): add mpp adapter with detect, quote, execute
leventilo Mar 19, 2026
572f01a
feat(protocols): export mpp adapter from protocols barrel
leventilo Mar 19, 2026
1335edc
feat(sdk): export diagnose-endpoint and related types from barrel
leventilo Mar 19, 2026
0049091
fix(sdk): populate mpp-methods when adapter matches in diagnose pipeline
leventilo Mar 19, 2026
3c08c17
feat(sdk): post probe on 404/405 to detect post-only paid endpoints
leventilo Mar 19, 2026
9dd2997
fix(sdk): add http_405 death reason, tests for post probe on 404/405
leventilo Mar 19, 2026
539b290
fix(cli): declare required env vars in openclaw manifest, add mpp
leventilo Mar 20, 2026
ac3ee23
chore(deps): update dependency astro to v5.18.1
renovate[bot] Mar 20, 2026
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
6 changes: 3 additions & 3 deletions integrations/openclaw/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
name: boltzpay
description: Pay for API data automatically — multi-protocol (x402 + L402), multi-chain
metadata: {"openclaw": {"emoji": "\u26a1", "requires": {"bins": ["npx"]}, "install": [{"id": "boltzpay-cli", "kind": "node", "label": "BoltzPay CLI"}]}}
description: Pay for API data automatically — multi-protocol (x402 + L402 + MPP), multi-chain
metadata: {"openclaw": {"emoji": "\u26a1", "requires": {"bins": ["npx"], "env": ["COINBASE_API_KEY_ID", "COINBASE_API_KEY_SECRET", "COINBASE_WALLET_SECRET"]}, "install": [{"id": "boltzpay-cli", "kind": "node", "label": "BoltzPay CLI"}]}}
---

# BoltzPay — Paid API Access for AI Agents

BoltzPay lets AI agents pay for API data automatically. It supports multiple payment protocols (x402 and L402) and multiple blockchains (Base and Solana), paying with USDC or Bitcoin Lightning. Agents can discover, evaluate, and purchase API data in a single workflow.
BoltzPay lets AI agents pay for API data automatically. It supports multiple payment protocols (x402, L402, and MPP) and multiple blockchains (Base, Solana, and Tempo), paying with USDC, Bitcoin Lightning, or Stripe. Agents can discover, evaluate, and purchase API data in a single workflow.

## Quick Start

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export {
} from "./shared/payment-errors";
export type {
EndpointInputHints,
MppMethodQuote,
ProtocolAdapter,
ProtocolQuote,
ProtocolResult,
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/shared/protocol-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ export interface EndpointInputHints {
readonly outputExample?: unknown;
}

export interface MppMethodQuote {
readonly method: string;
readonly intent: string;
readonly amount: Money;
readonly currency: string;
readonly network: string | undefined;
readonly recipient: string | undefined;
}

export interface ProtocolQuote {
readonly amount: Money;
readonly protocol: string;
Expand All @@ -17,6 +26,9 @@ export interface ProtocolQuote {
readonly scheme: string;
readonly allAccepts?: readonly AcceptOption[];
readonly inputHints?: EndpointInputHints;
readonly allMethods?: readonly MppMethodQuote[];
readonly selectedMethod?: string;
readonly priceUnknown?: boolean;
}

export interface ProtocolResult {
Expand Down
3 changes: 2 additions & 1 deletion packages/protocols/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,18 @@
"dependencies": {
"@boltzpay/core": "workspace:*",
"@coinbase/cdp-sdk": "^1.44.1",
"@getalby/sdk": "^7.0.0",
"@solana/kit": "^6.1.0",
"@x402/core": "^2.6.0",
"@x402/evm": "^2.6.0",
"@x402/fetch": "^2.6.0",
"@x402/svm": "^2.6.0",
"async-mutex": "^0.5.0",
"@getalby/sdk": "^7.0.0",
"light-bolt11-decoder": "^3.2.0"
},
"devDependencies": {
"@boltzpay/config": "workspace:*",
"mppx": "0.4.7",
"tsup": "^8.5.1",
"typescript": "^5.8.3",
"vitest": "^4.0.18"
Expand Down
12 changes: 12 additions & 0 deletions packages/protocols/src/adapter-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ export class L402CredentialsMissingError extends AdapterError {
}
}

export class MppQuoteError extends AdapterError {
constructor(message: string) {
super("mpp_quote_failed", message);
}
}

export class MppPaymentError extends AdapterError {
constructor(message: string, options?: ErrorOptions) {
super("mpp_payment_failed", message, options);
}
}

/**
* Thrown when all payment adapters fail during fallback execution.
* Contains all individual errors for diagnostic context.
Expand Down
12 changes: 12 additions & 0 deletions packages/protocols/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
export type { DeliveryAttemptResult } from "./adapter-error";
export { hasMppScheme, parseMppChallenges } from "./mpp/mpp-parsing";
export { MppMethodSelector } from "./mpp/mpp-method-selector";
export type { MppResolvedMethod } from "./mpp/mpp-method-selector";
export { MppAdapter } from "./mpp/mpp-adapter";
export { buildMppQuote } from "./mpp/mpp-quote-builder";
export type {
MppChallenge,
MppParseResult,
MppRequest,
} from "./mpp/mpp-types";
export {
AdapterError,
AggregatePaymentError,
CdpProvisioningError,
L402CredentialsMissingError,
L402PaymentError,
L402QuoteError,
MppPaymentError,
MppQuoteError,
X402PaymentError,
X402QuoteError,
} from "./adapter-error";
Expand Down
130 changes: 130 additions & 0 deletions packages/protocols/src/mpp/mpp-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type {
ProtocolAdapter,
ProtocolQuote,
ProtocolResult,
} from "@boltzpay/core";
import { MppPaymentError, MppQuoteError } from "../adapter-error";
import type { ResponseAwareAdapter } from "../router/protocol-router";
import type { AdapterTimeouts } from "../x402/x402-adapter";
import type { MppMethodSelector } from "./mpp-method-selector";
import { hasMppScheme, parseMppChallenges } from "./mpp-parsing";
import { buildMppQuote } from "./mpp-quote-builder";

const DEFAULT_DETECT_MS = 10_000;
const DEFAULT_QUOTE_MS = 15_000;
const DEFAULT_PAYMENT_MS = 30_000;
const HTTP_PAYMENT_REQUIRED = 402;

export class MppAdapter implements ResponseAwareAdapter {
readonly name = "mpp";
private readonly timeouts: Required<AdapterTimeouts>;

constructor(
private readonly methodSelector: MppMethodSelector,
private readonly validateUrl: (url: string) => void,
timeouts?: AdapterTimeouts,
) {
this.timeouts = {
detect: timeouts?.detect ?? DEFAULT_DETECT_MS,
quote: timeouts?.quote ?? DEFAULT_QUOTE_MS,
payment: timeouts?.payment ?? DEFAULT_PAYMENT_MS,
};
}

async detect(
url: string,
_headers?: Record<string, string>,
): Promise<boolean> {
this.validateUrl(url);
let response: Response;
try {
response = await fetch(url, {
method: "GET",
redirect: "manual",
signal: AbortSignal.timeout(this.timeouts.detect),
});
} catch (err) {
const msg = err instanceof Error ? err.message : "Network error";
throw new MppQuoteError(`Cannot reach endpoint: ${msg}`);
}
if (response.status !== HTTP_PAYMENT_REQUIRED) {
return false;
}
const wwwAuth = response.headers.get("www-authenticate");
if (!wwwAuth) {
return false;
}
return hasMppScheme(wwwAuth);
}

async quote(
url: string,
_headers?: Record<string, string>,
): Promise<ProtocolQuote> {
this.validateUrl(url);
const response = await this.fetchForQuote(url);
return this.extractQuote(response);
}

async quoteFromResponse(response: Response): Promise<ProtocolQuote | null> {
if (response.status !== HTTP_PAYMENT_REQUIRED) {
return null;
}
const wwwAuth = response.headers.get("www-authenticate");
if (!wwwAuth || !hasMppScheme(wwwAuth)) {
return null;
}
try {
const { challenges } = parseMppChallenges(wwwAuth);
return buildMppQuote(challenges, this.methodSelector);
} catch {
return null;
}
}

async execute(_request: {
readonly url: string;
readonly method: string;
readonly headers: Record<string, string>;
readonly body: Uint8Array | undefined;
readonly amount: import("@boltzpay/core").Money;
}): Promise<ProtocolResult> {
throw new MppPaymentError(
"MPP execute() not implemented -- see Phase 18",
);
}

private async fetchForQuote(url: string): Promise<Response> {
let response: Response;
try {
response = await fetch(url, {
method: "GET",
redirect: "error",
signal: AbortSignal.timeout(this.timeouts.quote),
});
} catch (err) {
const msg = err instanceof Error ? err.message : "Network error";
throw new MppQuoteError(`Cannot reach endpoint: ${msg}`);
}
if (response.status !== HTTP_PAYMENT_REQUIRED) {
throw new MppQuoteError(
`Expected 402 status, got ${response.status}`,
);
}
return response;
}

private extractQuote(response: Response): ProtocolQuote {
const wwwAuth = response.headers.get("www-authenticate");
if (!wwwAuth || !hasMppScheme(wwwAuth)) {
throw new MppQuoteError(
"No MPP payment information in 402 response",
);
}
const { challenges } = parseMppChallenges(wwwAuth);
if (challenges.length === 0) {
throw new MppQuoteError("No MPP challenges found in response");
}
return buildMppQuote(challenges, this.methodSelector);
}
}
102 changes: 102 additions & 0 deletions packages/protocols/src/mpp/mpp-method-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { Money } from "@boltzpay/core";

export interface MppResolvedMethod {
readonly method: string;
readonly intent: string;
readonly amount: Money;
readonly currency: string;
readonly network: string | undefined;
readonly recipient: string | undefined;
}

const WALLET_TO_MPP_METHOD: Readonly<Record<string, readonly string[]>> = {
nwc: ["lightning"],
"stripe-mpp": ["stripe"],
tempo: ["tempo"],
"visa-mpp": ["card"],
} as const;

function buildMethodToWalletMap(): ReadonlyMap<string, readonly string[]> {
const reverse = new Map<string, string[]>();
for (const [walletType, methods] of Object.entries(WALLET_TO_MPP_METHOD)) {
for (const m of methods) {
const existing = reverse.get(m) ?? [];
existing.push(walletType);
reverse.set(m, existing);
}
}
return reverse;
}

const METHOD_TO_WALLET = buildMethodToWalletMap();

function sortByCheapest(
a: MppResolvedMethod,
b: MppResolvedMethod,
): number {
const aZero = a.amount.isZero();
const bZero = b.amount.isZero();
if (aZero && !bZero) return 1;
if (!aZero && bZero) return -1;
if (a.amount.cents < b.amount.cents) return -1;
if (a.amount.cents > b.amount.cents) return 1;
return 0;
}

export class MppMethodSelector {
private readonly configuredWalletTypes: ReadonlySet<string>;
private readonly preferredMethods: readonly string[];

constructor(
configuredWalletTypes: ReadonlySet<string>,
preferredMethods: readonly string[],
) {
this.configuredWalletTypes = configuredWalletTypes;
this.preferredMethods = preferredMethods;
}

select(methods: readonly MppResolvedMethod[]): MppResolvedMethod {
if (this.preferredMethods.length > 0) {
return this.selectByPreference(methods);
}

if (this.configuredWalletTypes.size > 0) {
return this.selectByWallet(methods);
}

return this.selectCheapest(methods);
}

private selectByPreference(
methods: readonly MppResolvedMethod[],
): MppResolvedMethod {
for (const preferred of this.preferredMethods) {
const match = methods.find((m) => m.method === preferred);
if (match) return match;
}
return this.selectCheapest(methods);
}

private selectByWallet(
methods: readonly MppResolvedMethod[],
): MppResolvedMethod {
const compatible = methods.filter((m) => this.isWalletCompatible(m.method));
if (compatible.length === 0) {
return this.selectCheapest(methods);
}
return this.selectCheapest(compatible);
}

private isWalletCompatible(methodName: string): boolean {
const walletTypes = METHOD_TO_WALLET.get(methodName);
if (!walletTypes) return false;
return walletTypes.some((wt) => this.configuredWalletTypes.has(wt));
}

private selectCheapest(
methods: readonly MppResolvedMethod[],
): MppResolvedMethod {
const sorted = [...methods].sort(sortByCheapest);
return sorted[0]!;
}
}
Loading
Loading