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.
195 lines • 9.12 kB
JavaScript
/**
* The Lightning rail's x402 buyer: pays the x402 Foundation's `exact` scheme on
* Bitcoin Lightning (`lnbtc`) from the agent's own Lightning wallet.
*
* The seller's 402 carries an `accepts[]` entry on an `lnbtc:` network whose
* `extra.invoice` is a BOLT11 invoice committing (by description hash) to the
* exact request. The vendored client scheme recomputes that request hash from
* the request this buyer is about to send, refuses to pay anything bound to
* another request or signed by another key, pays, and checks the preimage.
* The paid retry is the same request plus `PAYMENT-SIGNATURE`.
*
* Run402 offers it on `POST /generate-image/v1` (spec `x402-lightning-exact`);
* the Lightning fetch prefers it there over MPP Lightning. The preimage and the
* pairing secret never reach a log or an error.
*/
import { decodeInvoice } from "./vendor/x402-lnbtc/bolt11.js";
import { httpRequestBinding } from "./vendor/x402-lnbtc/binding.js";
import { LnbtcError } from "./vendor/x402-lnbtc/constants.js";
import { ExactLnbtcScheme } from "./vendor/x402-lnbtc/exact/client/scheme.js";
import { NwcError } from "./nwc.js";
/** Run402 surfaces that offer x402 `exact` on `lnbtc`. */
const X402_LNBTC_PATHS = [/^\/generate-image\/v1$/];
/** The fetch default for a string body without an explicit content type. */
const STRING_BODY_CONTENT_TYPE = "text/plain;charset=UTF-8";
function requestUrl(input) {
return typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
}
/** A POST to a Run402 surface that offers x402 on Lightning. */
export function isX402LightningCandidate(input, init) {
const method = (init?.method ?? (typeof input === "object" && "method" in input ? input.method : "GET")).toUpperCase();
if (method !== "POST")
return false;
try {
return X402_LNBTC_PATHS.some((pattern) => pattern.test(new URL(requestUrl(input)).pathname));
}
catch {
return false;
}
}
/** The x402 v2 challenge's `exact` entry on an `lnbtc:` network with an invoice, or null. */
/** The x402 network ids of every payable `lnbtc` entry in a 402 (empty when there is none). */
export function x402LightningNetworks(response) {
const paymentRequired = decodePaymentRequired(response);
if (!paymentRequired)
return [];
return [...new Set(paymentRequired.accepts.filter(isPayableLnbtcEntry).map((entry) => entry.network))];
}
function decodePaymentRequired(response) {
if (response.status !== 402)
return null;
const header = response.headers.get("payment-required");
if (!header)
return null;
let decoded;
try {
decoded = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
}
catch {
return null;
}
const paymentRequired = decoded;
if (!paymentRequired || paymentRequired.x402Version !== 2 || !Array.isArray(paymentRequired.accepts))
return null;
return paymentRequired;
}
function isPayableLnbtcEntry(entry) {
return Boolean(entry) && entry.scheme === "exact" &&
typeof entry.network === "string" && entry.network.startsWith("lnbtc:") &&
typeof entry.extra?.invoice === "string" && entry.extra.invoice.length > 0;
}
/**
* The payable `lnbtc` entry of a 402: on `network` when one is named (the
* wallet's), else the first one. Null when the 402 carries none.
*/
export function readX402LightningOffer(response, network) {
const paymentRequired = decodePaymentRequired(response);
if (!paymentRequired)
return null;
const requirements = paymentRequired.accepts.find((entry) => isPayableLnbtcEntry(entry) && (network === undefined || entry.network === network));
return requirements ? { paymentRequired, requirements } : null;
}
/** The body bytes exactly as fetch will send them, or null when they cannot be known up front. */
function bodyBytes(body) {
if (body === undefined || body === null)
return new Uint8Array(0);
if (typeof body === "string")
return new TextEncoder().encode(body);
if (body instanceof Uint8Array)
return body;
if (body instanceof ArrayBuffer)
return new Uint8Array(body);
return null;
}
/**
* The request headers made explicit: a string body without a content type is
* sent by fetch as `text/plain;charset=UTF-8`, and the request hash must bind
* the value actually sent, so it is set here rather than left to fetch.
*/
export function explicitRequestHeaders(init) {
const headers = new Headers(init?.headers ?? {});
if (typeof init?.body === "string" && !headers.has("content-type")) {
headers.set("content-type", STRING_BODY_CONTENT_TYPE);
}
return headers;
}
/** Pay the offer and send the paid retry. */
export async function payX402Lightning(options) {
const { input, init, offer, wallet, fail } = options;
const url = requestUrl(input);
const method = (init?.method ?? "POST").toUpperCase();
const bytes = bodyBytes(init?.body);
if (!bytes) {
throw fail("LIGHTNING_BODY_NOT_REPLAYABLE", "An x402 Lightning payment binds the exact body bytes; a streamed body cannot be paid for.");
}
const headers = explicitRequestHeaders(init);
const params = offer.requirements.extra.requestBindingParams;
const boundHeaders = Array.isArray(params?.headers) ? params.headers.filter((name) => typeof name === "string") : [];
const amountMsat = BigInt(offer.requirements.amount);
await options.checkCaps(Number((amountMsat + 999n) / 1000n));
const payer = {
payInvoice: async (invoice) => {
const decoded = decodeInvoice(invoice);
let preimage;
try {
preimage = (await wallet.payInvoice(invoice)).preimage;
}
catch (error) {
// Unknown outcome: resolve by hash before ever paying again.
if (error instanceof NwcError && (error.code === "TIMEOUT" || error.code === "RELAY_CLOSED" || error.code === "RELAY_UNREACHABLE")) {
const looked = await wallet.lookupInvoice(decoded.paymentHash).catch(() => null);
if (!looked?.preimage) {
throw fail("LIGHTNING_PAYMENT_OUTCOME_UNKNOWN", "The Lightning payment's outcome is unknown; check the wallet before paying again.", { payment_hash: decoded.paymentHash });
}
preimage = looked.preimage;
}
else if (error instanceof NwcError) {
throw fail(`LIGHTNING_${error.code}`, `The Lightning wallet refused the payment (${error.code}).`, { payment_hash: decoded.paymentHash, next_action: "pay over x402 (run402 init --switch-rail)" });
}
else {
throw error;
}
}
return { invoice, paymentHash: decoded.paymentHash, amountMsat: decoded.amountMsat, status: "paid", preimage };
},
};
const scheme = new ExactLnbtcScheme({
payer,
...(options.clock ? { clock: options.clock } : {}),
requestBinding: () => httpRequestBinding({
method,
url,
body: bytes,
boundHeaders,
getHeader: (name) => (headers.has(name) ? [headers.get(name)] : undefined),
}),
});
let payload;
try {
const abort = await scheme.schemeHooks.onBeforePaymentCreation?.({
paymentRequired: offer.paymentRequired,
selectedRequirements: offer.requirements,
});
if (abort && "abort" in abort && abort.abort) {
throw new LnbtcError(abort.reason);
}
const result = await scheme.createPaymentPayload(2, offer.requirements);
payload = result.payload;
}
catch (error) {
if (error instanceof LnbtcError) {
const mismatch = error.reason === "invalid_exact_lnbtc_request_mismatch" ||
error.reason === "invalid_exact_lnbtc_invoice_request_mismatch";
throw fail(mismatch ? "LIGHTNING_REQUEST_BINDING_MISMATCH" : "LIGHTNING_X402_INVOICE_REFUSED", mismatch
? "The seller's Lightning invoice is bound to a different request than the one being sent; nothing was paid."
: `The seller's x402 Lightning offer failed validation (${error.reason}); nothing was paid.`, { reason: error.reason });
}
throw error;
}
const paymentHash = decodeInvoice(String(offer.requirements.extra.invoice)).paymentHash;
options.onPaid?.(paymentHash, {
network: offer.requirements.network,
amountMsat: Number(offer.requirements.amount),
payTo: offer.requirements.payTo,
resourceUrl: url,
});
const signature = Buffer.from(JSON.stringify({
x402Version: 2,
resource: offer.paymentRequired.resource,
accepted: offer.requirements,
payload,
})).toString("base64");
headers.set("payment-signature", signature);
return options.baseFetch(input, { ...init, headers });
}
//# sourceMappingURL=lightning-x402-lnbtc.js.map