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
JavaScript
/**
* 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