UNPKG

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.

145 lines • 5.42 kB
/** * Every payment a 402 offers, whichever protocol states it: x402 entries from * `PAYMENT-REQUIRED`, MPP challenges from `WWW-Authenticate: Payment`. The * wrong-rail error lists these and names the rail switch that would pay the * first one (spec `tenant-x402-payment-client-surface`). * * Parsed without the paid stack: an x402-only install has no `mppx`, and the * error still has to say what the seller wanted. */ const LNBTC_PREFIX = "lnbtc:"; export function paymentOffers(response) { return [...x402Offers(response), ...mppOffers(response)]; } export function x402Offers(response) { const header = response.headers.get("PAYMENT-REQUIRED"); if (!header) return []; try { // base64url decoding accepts both alphabets, so it reads either seller encoding. const decoded = JSON.parse(Buffer.from(header, "base64url").toString("utf8")); if (!Array.isArray(decoded.accepts)) return []; return decoded.accepts.flatMap((accept) => { const entry = accept; return typeof entry?.scheme === "string" ? [{ protocol: "x402", method: entry.scheme, network: typeof entry.network === "string" ? entry.network : null }] : []; }); } catch { return []; } } /** The `Payment` challenges in `WWW-Authenticate`, with their decoded `request`. */ export function mppChallenges(response) { const header = response.headers.get("WWW-Authenticate"); if (!header) return []; return splitPaymentChallenges(header).flatMap((params) => { if (!params.method || !params.intent) return []; return [{ ...(params.id ? { id: params.id } : {}), method: params.method, intent: params.intent, request: decodeRequest(params.request), }]; }); } export function mppOffers(response) { return mppChallenges(response).map((challenge) => ({ protocol: "mpp", method: challenge.method, intent: challenge.intent, network: mppNetwork(challenge), })); } /** `eip155:<chainId>` for a Tempo charge; the request's own network otherwise. */ function mppNetwork(challenge) { const details = challenge.request?.methodDetails; if (challenge.method === "tempo" && (typeof details?.chainId === "number" || typeof details?.chainId === "string")) { return `eip155:${String(details.chainId)}`; } return typeof details?.network === "string" ? details.network : null; } /** The rail that pays an offer: x402 EVM entries, MPP Tempo, and Lightning (either protocol). */ export function railForOffer(offer) { if (offer.method === "lightning" || offer.network?.startsWith(LNBTC_PREFIX)) return "lightning"; if (offer.protocol === "mpp") return offer.method === "tempo" ? "mpp" : null; return offer.method === "exact" ? "x402" : null; } /** The first offer's rail, when it is not the configured one. */ export function switchRailFor(offers, configured) { for (const offer of offers) { const rail = railForOffer(offer); if (rail && rail !== configured) return rail; } return null; } /** * RFC 9110 auth-params for every `Payment` scheme in the header. A header may * carry several challenges, comma-joined; a new challenge starts where a * scheme token is followed by a space instead of `=`. */ function splitPaymentChallenges(header) { const challenges = []; let index = 0; while (index < header.length) { index = skip(header, index, " \t,"); const token = readToken(header, index); if (!token) break; index += token.length; if (header[index] !== "=") { challenges.push({ scheme: token.toLowerCase(), params: {} }); continue; } const current = challenges[challenges.length - 1]; const parsed = readParamValue(header, index + 1); index = parsed.next; if (current) current.params[token.toLowerCase()] = parsed.value; } return challenges.filter((challenge) => challenge.scheme === "payment").map((challenge) => challenge.params); } function skip(value, index, chars) { while (index < value.length && chars.includes(value[index])) index += 1; return index; } function readToken(value, index) { const match = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+/.exec(value.slice(index)); return match ? match[0] : ""; } function readParamValue(value, index) { index = skip(value, index, " \t"); if (value[index] !== "\"") { const token = /^[^,\s]*/.exec(value.slice(index))[0]; return { value: token, next: index + token.length }; } let out = ""; let cursor = index + 1; while (cursor < value.length && value[cursor] !== "\"") { if (value[cursor] === "\\" && cursor + 1 < value.length) cursor += 1; out += value[cursor]; cursor += 1; } return { value: out, next: cursor + 1 }; } function decodeRequest(encoded) { if (!encoded) return null; try { const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); return value && typeof value === "object" && !Array.isArray(value) ? value : null; } catch { return null; } } //# sourceMappingURL=payment-offers.js.map