run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
352 lines • 17.1 kB
JavaScript
/**
* `pay.fetch` on the `mpp` rail: buy an arbitrary URL whose 402 carries an
* MPP Tempo charge challenge (spec `tenant-x402-payment-client-surface`,
* decision D-tenant-mpp-priced-routes-9).
*
* Send the request; on 402 select the first offered Tempo chain the wallet
* covers (`orderTempoCandidates`, through mppx's own selection), apply the USD
* spend guard to the selected challenge before anything is signed, sign, and
* retry the identical request with `Authorization: Payment`. The attempt is
* journaled before the signed request leaves.
*
* An MPP payment's identity is its credential, not the caller key: the
* gateway resolves a re-presented credential to the original payment and
* treats `Idempotency-Key` as correlation only. So the signed credential stays
* in this instance's memory for the identical request, and an ambiguous or
* pending outcome is answered by re-presenting it, never by signing a second
* one. A clean refusal (no funds moved) forgets it.
*/
import { createHash } from "node:crypto";
import { LocalError } from "../errors.js";
import { PaymentBuyerError, gatewayPaymentBuyerError, isTrustedRun402PendingResponse, merchantReceiptRequiredError, payResponseMetadata, paymentExceedsMaxError, readPaymentErrorEnvelope, responseSignalsReplay, } from "../namespaces/pay.js";
import { createFilePaymentAttemptStore, createPaymentAttemptId, requestSummary, withPaymentAttemptHeader, } from "./payment-attempts.js";
import { mppChallenges, paymentOffers } from "./payment-offers.js";
import { PATH_USD, tempoChainById } from "./tempo.js";
/** Bounded waits for the gateway's "settlement still pending" answer to the paid request. */
export const MPP_PENDING_RETRY_LIMIT = 4;
const PENDING_RETRY_MIN_MS = 1_000;
const PENDING_RETRY_MAX_MS = 10_000;
export function createMppPayExecutor(options) {
const held = new Map();
const store = options.store ?? createFilePaymentAttemptStore();
const makeId = options.createAttemptId ?? createPaymentAttemptId;
const now = options.now ?? (() => new Date().toISOString());
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
return async (url, init, payOptions) => {
const fingerprint = await options.fingerprint(url, init, payOptions.maxUsdMicros, payOptions.requireReceipt);
const request = new Request(url, init);
let hold = held.get(fingerprint);
if (!hold) {
const first = await options.baseFetch(request.clone());
if (first.status !== 402) {
return {
response: first,
payment: null,
outcome: "not_required",
replay: await responseSignalsReplay(first, payOptions.idempotencyKey !== undefined),
...payResponseMetadata(first),
};
}
hold = await signForChallenge(first, request, payOptions, options, makeId);
held.set(fingerprint, hold);
}
const record = journalIntent(store, hold.attemptId, url, init, now);
write(store, record, now, "submitting", "in_progress", { provider_started_at: now() });
let response;
try {
// From here a thrown transport error leaves the outcome unknown.
response = await sendPaid(request, hold, options, sleep);
}
catch (cause) {
writeBestEffort(store, record, now, "ambiguous", "ambiguous", { last_error_code: "MPP_PAYMENT_OUTCOME_AMBIGUOUS" });
throw outcomeUnknown("The MPP payment request failed after the credential was sent; its outcome is unknown.", hold, cause);
}
return settleOutcome(url, response, hold, payOptions.idempotencyKey !== undefined, options, {
forget: () => held.delete(fingerprint),
journal: (state, mutation, extra) => writeBestEffort(store, record, now, state, mutation, extra),
});
};
}
async function signForChallenge(response, request, payOptions, options, makeId) {
const tempoOffered = mppChallenges(response).some((challenge) => challenge.method === "tempo" && challenge.intent === "charge");
if (!tempoOffered) {
await response.body?.cancel().catch(() => undefined);
throw options.unsupported(paymentOffers(response));
}
if (payOptions.requireReceipt) {
// MPP carries a protocol receipt, never a wallet-rooted merchant receipt.
await response.body?.cancel().catch(() => undefined);
throw merchantReceiptRequiredError({ protocol: "mpp" });
}
const prepared = await options.prepare(response, { method: request.method, headers: request.headers });
await response.body?.cancel().catch(() => undefined);
const offer = tempoChargeOffer(prepared.challenge);
if (offer.amountUsdMicros > payOptions.maxUsdMicros) {
throw paymentExceedsMaxError(offer.amountUsdMicros, payOptions.maxUsdMicros);
}
const credential = await prepared.createCredential();
// Keep exactly the header(s) mppx sets for this challenge; the request itself is rebuilt from `request`.
const credentialHeaders = [...new Headers(prepared.setCredential({ headers: new Headers() }, credential).headers).entries()];
return { credentialHeaders, challenge: offer, attemptId: makeId() };
}
/** Read the selected Tempo charge: pathUSD only, so base units are USD micros. */
function tempoChargeOffer(challenge) {
const request = (challenge.request ?? {});
const chain = tempoChainById(request.methodDetails?.chainId);
const amount = typeof request.amount === "string" && /^\d+$/.test(request.amount) ? Number(request.amount) : NaN;
const currency = typeof request.currency === "string" ? request.currency.toLowerCase() : "";
if (!chain || !Number.isSafeInteger(amount) || currency !== PATH_USD || typeof request.recipient !== "string") {
throw new PaymentBuyerError({
code: "PAYMENT_NETWORK_UNSUPPORTED",
message: "The MPP Tempo challenge is not a pathUSD charge on a Tempo chain this wallet pays.",
fundsMoved: false,
details: {
protocol: "mpp",
chain_id: request.methodDetails?.chainId ?? null,
currency: request.currency ?? null,
},
nextActions: [{ type: "edit_request", why: "Use a priced endpoint that charges pathUSD on Tempo mainnet or Moderato." }],
});
}
return { chainId: chain.id, network: chain.network, amountUsdMicros: amount, recipient: request.recipient };
}
async function sendPaid(request, hold, options, sleep) {
const paidRequest = () => {
const headers = new Headers(request.headers);
for (const [name, value] of hold.credentialHeaders)
headers.set(name, value);
const [, paidInit] = withPaymentAttemptHeader(request.url, { headers }, hold.attemptId, true);
return new Request(request.clone(), paidInit);
};
let response = await options.baseFetch(paidRequest());
for (let waits = 0; waits < MPP_PENDING_RETRY_LIMIT; waits += 1) {
const delayMs = await pendingRetryDelay(request.url, response, options.allowTestLocalhost === true);
if (delayMs === null)
break;
await response.body?.cancel().catch(() => undefined);
await sleep(delayMs);
// The same credential: the gateway resolves it to the same payment.
response = await options.baseFetch(paidRequest());
}
return response;
}
async function pendingRetryDelay(url, response, allowTestLocalhost) {
const envelope = await readPaymentErrorEnvelope(response);
const pending = isTrustedRun402PendingResponse({
requestUrl: url,
response,
envelope,
paymentBearing: true,
redirectsDisabled: true,
allowTestLocalhost,
});
if (!pending)
return null;
const seconds = Number(response.headers.get("retry-after"));
const ms = Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : PENDING_RETRY_MIN_MS;
return Math.min(PENDING_RETRY_MAX_MS, Math.max(PENDING_RETRY_MIN_MS, ms));
}
async function settleOutcome(url, response, hold, keyed, options, hooks) {
const receipt = readPaymentReceipt(response);
if (response.ok || receipt) {
// A Payment-Receipt on a non-2xx proves the transfer: paid, delivery failed.
hooks.journal("completed", "completed", { response_status: response.status });
return paidResult(url, response, hold, receipt, options.payer, keyed);
}
const envelope = await readPaymentErrorEnvelope(response);
const trusted = envelope ? gatewayPaymentBuyerError(response, envelope) : null;
if (trusted) {
const pending = trusted.code === "PAYMENT_INTENT_PENDING";
hooks.journal(pending ? "intent_pending" : trusted.fundsMoved === false ? "failed" : "ambiguous", pending || trusted.fundsMoved !== false ? "ambiguous" : "not_started", { response_status: response.status, last_error_code: trusted.code });
if (trusted.fundsMoved === false)
hooks.forget();
throw trusted;
}
const fundsMoved = refusalFundsMoved(response, envelope);
hooks.journal(fundsMoved === false ? "failed" : "ambiguous", fundsMoved === false ? "not_started" : "ambiguous", {
response_status: response.status,
...(typeof envelope?.code === "string" ? { last_error_code: envelope.code } : {}),
});
if (fundsMoved === false) {
hooks.forget();
throw new PaymentBuyerError({
code: "PAYMENT_SETTLEMENT_FAILED",
message: "The seller refused the MPP Tempo credential; no funds moved.",
fundsMoved: false,
details: { protocol: "mpp", network: hold.challenge.network, upstream_status: response.status, ...upstreamCode(envelope) },
nextActions: [{ type: "retry", why: "No funds moved; retry to receive a fresh challenge." }],
safeToRetry: true,
});
}
throw outcomeUnknown("The seller answered the MPP payment without a receipt; whether the transfer landed is unknown.", hold, null, {
upstream_status: response.status,
...upstreamCode(envelope),
});
}
/**
* A 402 is the seller re-challenging: it did not accept the credential. This
* client pays Tempo in pull mode (it signs, the seller broadcasts), so a
* refusal moved nothing unless the seller says otherwise. Any other non-2xx
* without a receipt is unknown.
*/
function refusalFundsMoved(response, envelope) {
const details = envelope?.details;
const declared = envelope?.funds_moved ?? details?.funds_moved;
if (declared === true || declared === false || declared === "unknown")
return declared;
return response.status === 402 ? false : "unknown";
}
function upstreamCode(envelope) {
return typeof envelope?.code === "string" ? { upstream_code: envelope.code } : {};
}
function outcomeUnknown(message, hold, cause, details = {}) {
return new PaymentBuyerError({
code: "PAYMENT_SETTLEMENT_FAILED",
message,
fundsMoved: "unknown",
details: { protocol: "mpp", network: hold.challenge.network, payment_attempt_id: hold.attemptId, ...details },
nextActions: [{
type: "retry",
request: "repeat_identical",
why: "Repeat the identical request from this client: it re-presents the same credential, which the seller resolves to the same payment and never charges twice.",
}],
retryable: true,
safeToRetry: false,
...(cause !== null ? { cause } : {}),
});
}
async function paidResult(url, response, hold, receipt, payer, keyed) {
const metadata = payResponseMetadata(response);
const deduplicated = metadata.deduplicated ?? false;
// MPP carries no merchant receipt and no signer authorization, so these instants are absent.
const noInstant = null;
const payment = {
protocol: "mpp",
method: "tempo",
network: hold.challenge.network,
paymentId: metadata.paymentId,
amountUsdMicros: hold.challenge.amountUsdMicros,
asset: PATH_USD,
payer,
payTo: hold.challenge.recipient,
transaction: receipt?.reference ?? "",
resourceUrl: url,
settlement: { status: receipt ? "verified" : "unavailable" },
fundsMoved: metadata.fundsMoved ?? (deduplicated ? false : true),
deduplicated,
delivery: { status: response.ok ? "fulfilled" : "failed", replay: deduplicated },
offer: { status: "unavailable", resourceUrl: null, validUntil: null },
merchantReceipt: { status: "unavailable", claim: null, issuedAt: noInstant },
signerRelationship: { kind: null, merchantRoot: null, signer: null, authorizationExpiresAt: noInstant },
policy: { requireReceipt: false, status: "not_required" },
evidence: { offer: null, merchantReceipt: null, signerAuthorization: null, protocolReceipt: receipt },
};
return {
response,
payment,
outcome: deduplicated ? "already_settled" : "settled",
replay: deduplicated || await responseSignalsReplay(response, keyed),
...metadata,
};
}
/** The standard MPP `Payment-Receipt`: base64url JSON `{ method, reference, status, timestamp }`. */
function readPaymentReceipt(response) {
const header = response.headers.get("payment-receipt");
if (!header)
return null;
try {
const parsed = JSON.parse(Buffer.from(header, "base64url").toString("utf8"));
if (parsed.status !== "success" || typeof parsed.reference !== "string" || typeof parsed.method !== "string")
return null;
return {
method: parsed.method,
reference: parsed.reference,
status: parsed.status,
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : null,
};
}
catch {
return null;
}
}
function journalIntent(store, attemptId, url, init, now) {
const existing = safeRead(store, attemptId);
if (existing)
return existing;
const summary = requestSummary(url, init);
const at = now();
const record = {
version: 1,
payment_attempt_id: attemptId,
rail: "mpp",
state: "intent",
mutation_state: "not_started",
method: summary.method,
origin: summary.origin,
path_sha256: summary.path_sha256,
...(summary.caller_key_sha256 ? { caller_key_sha256: summary.caller_key_sha256 } : {}),
created_at: at,
updated_at: at,
};
try {
if (!store.claim(record)) {
throw new LocalError("The generated MPP payment attempt id already exists.", "reserving MPP payment attempt", { code: "MPP_ATTEMPT_ID_COLLISION" });
}
}
catch (cause) {
throw new PaymentBuyerError({
code: "PAYMENT_SETTLEMENT_FAILED",
message: "The MPP payment was not sent because its attempt could not be recorded.",
fundsMoved: false,
details: { protocol: "mpp", payment_attempt_id: attemptId },
nextActions: [{ type: "retry", why: "Nothing was sent; retry once the local config directory is writable." }],
safeToRetry: true,
cause,
});
}
return record;
}
function safeRead(store, attemptId) {
try {
return store.read(attemptId);
}
catch {
return null;
}
}
/** Before dispatch the write must land: an unrecorded signed request is never sent. */
function write(store, record, now, state, mutation, extra = {}) {
Object.assign(record, extra, { state, mutation_state: mutation, updated_at: now() });
try {
store.write({ ...record });
}
catch (cause) {
throw new PaymentBuyerError({
code: "PAYMENT_SETTLEMENT_FAILED",
message: "The MPP payment was not sent because its attempt could not be recorded.",
fundsMoved: false,
details: { protocol: "mpp", payment_attempt_id: record.payment_attempt_id },
nextActions: [{ type: "retry", why: "Nothing was sent; retry once the local config directory is writable." }],
safeToRetry: true,
cause,
});
}
}
function writeBestEffort(store, record, now, state, mutation, extra = {}) {
try {
write(store, record, now, state, mutation, extra);
}
catch {
// The outcome is already decided; the journal is a best-effort record of it.
}
}
/** Test seam: a stable fingerprint for callers without the paid-fetch one. */
export async function simpleMppFingerprint(url, init, maxUsdMicros, requireReceipt) {
const request = new Request(url, init);
const body = await request.clone().arrayBuffer();
return createHash("sha256")
.update(JSON.stringify([request.method, request.url, request.headers.get("idempotency-key"), maxUsdMicros, requireReceipt]))
.update(Buffer.from(body))
.digest("hex");
}
//# sourceMappingURL=mpp-pay-executor.js.map