// A custom x402 v2 server with no web framework: node:http + @x402/core, following the manual flow of
// x402's own "custom" server example, with Forge's framework-free core. Use this pattern with any
// framework Forge has no adapter for yet. The only Forge-specific lines are marked with (forge).
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { decodePaymentSignatureHeader, encodePaymentRequiredHeader, encodePaymentResponseHeader } from "@x402/core/http";
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { BODY_LIMIT, createForgeCore } from "@forgeintel/sdk/core"; // (forge)
const required = (name: string) => {
const value = process.env[name];
if (!value) throw new Error(`Set ${name}`);
return value;
};
const PAY_TO = required("PAY_TO");
const PUBLIC_URL = required("PUBLIC_URL");
const NETWORK = (process.env.NETWORK ?? "eip155:84532") as `${string}:${string}`; // Base Sepolia; eip155:8453 for mainnet
const FACILITATOR_URL = process.env.FACILITATOR_URL ?? "https://x402.org/facilitator";
const QUOTES = [
"Programs must be written for people to read, and only incidentally for machines to execute.",
"Simplicity is prerequisite for reliability.",
"Make it work, make it right, make it fast.",
];
const spec = {
openapi: "3.1.0",
info: { title: "Quotes", version: "1.0.0", "x-guidance": "GET /v1/quote returns a random quote about software ($0.001, x402)." },
servers: [{ url: PUBLIC_URL }],
paths: {
"/v1/quote": {
get: {
responses: {
"200": { description: "A quote.", content: { "application/json": { schema: { type: "object", properties: { quote: { type: "string" } } } } } },
"402": { description: "Payment required." },
},
},
},
},
};
// (forge) The core does everything the Express middleware does; this file does the wiring.
const forge = createForgeCore({
apiKey: required("FORGE_API_KEY"),
backendUrl: required("FORGE_BACKEND_URL"),
publicUrl: PUBLIC_URL,
openapi: { document: spec }, // served enriched at GET /openapi.json by forge.route()
});
const resourceServer = new x402ResourceServer(new HTTPFacilitatorClient({ url: FACILITATOR_URL })).register(NETWORK, new ExactEvmScheme());
await resourceServer.initialize();
const paidRoutes: Record<string, { price: string; description: string; handler: () => unknown }> = {
"GET /v1/quote": {
price: "$0.001",
description: "A random quote about software. JSON.",
handler: () => ({ quote: QUOTES[Math.floor(Math.random() * QUOTES.length)] }),
},
};
function send(res: ServerResponse, status: number, headers: Record<string, string>, body?: unknown) {
res.writeHead(status, body === undefined ? headers : { ...headers, "Content-Type": "application/json" });
res.end(body === undefined ? undefined : JSON.stringify(body));
}
async function readJson(req: IncomingMessage): Promise<unknown> {
const chunks: Buffer[] = [];
let size = 0;
for await (const chunk of req as AsyncIterable<Buffer>) {
size += chunk.length;
if (size > BODY_LIMIT) throw new Error("body_too_large");
chunks.push(chunk);
}
const text = Buffer.concat(chunks).toString("utf8").trim();
return text ? JSON.parse(text) : {};
}
const server = createServer(async (req, res) => {
try {
const url = new URL(req.url ?? "/", PUBLIC_URL);
const method = req.method ?? "GET";
const header = (name: string) => {
const value = req.headers[name.toLowerCase()];
return Array.isArray(value) ? value[0] : value;
};
// (forge) Forge's own routes: /feedback, /feedback/rate, /feedback/summary and /openapi.json.
const own = await forge.route({ method, path: url.pathname, header, query: (name) => url.searchParams.get(name) ?? undefined, json: () => readJson(req) });
if (own) return send(res, own.status, own.headers, own.body);
if (method === "GET" && url.pathname === "/health") return send(res, 200, {}, { status: "ok" });
const route = paidRoutes[`${method} ${url.pathname}`];
if (!route) return send(res, 404, {}, { error: "not_found" });
const call = forge.call({ method, path: url.pathname, header }); // (forge)
res.on("finish", () => call.finish(res.statusCode)); // (forge) report events
// (forge) Read the agent's self-reported agent_* query parameters and drop them before your own logic.
// For JSON bodies, pass the parsed body through call.requestBody(body) the same way.
const query = new URL(call.requestUrl(`${url.pathname}${url.search}`), PUBLIC_URL).searchParams;
void query; // this route takes no parameters of its own
const [requirements] = await resourceServer.buildPaymentRequirements({ scheme: "exact", price: route.price, network: NETWORK, payTo: PAY_TO });
const signature = header("payment-signature");
if (!signature) {
const paymentRequired = await resourceServer.createPaymentRequiredResponse(
[requirements],
{ url: url.href, description: route.description, mimeType: "application/json" },
"Payment required",
);
const challenge = encodePaymentRequiredHeader(paymentRequired);
// (forge) call.headers() returns the challenge with the rating sentence and the forge-feedback extension added.
return send(res, 402, { "PAYMENT-REQUIRED": challenge, ...call.headers(402, challenge) }, call.json(402, {}));
}
const payment = decodePaymentSignatureHeader(signature);
const verified = await resourceServer.verifyPayment(payment, requirements);
if (!verified.isValid) return send(res, 402, {}, { error: "invalid_payment", reason: verified.invalidReason });
const data = route.handler();
const settled = await resourceServer.settlePayment(payment, requirements);
if (!settled.success) return send(res, 402, {}, { error: "settlement_failed" });
// (forge) call.json() adds feedback_id, feedback_url and rate_this_call. call.headers() adds Forge-Feedback-Id,
// and returns the receipt (PAYMENT-RESPONSE) with the forge-feedback extension added.
const receipt = encodePaymentResponseHeader(settled);
send(res, 200, { "PAYMENT-RESPONSE": receipt, ...call.headers(200, undefined, receipt) }, call.json(200, data));
} catch (error) {
console.error(error);
if (!res.headersSent) send(res, 500, {}, { error: "internal_error" });
}
});
const port = Number(process.env.PORT ?? 4023);
server.listen(port, () => console.log(`node:http example on http://localhost:${port}`));
process.on("SIGTERM", () => server.close(() => void forge.shutdown())); // (forge) flush pending events