x402 v1 (x402-express 1.x) | x402 v2 (@x402/express) | |
|---|---|---|
| Challenge | JSON body, accepts[].description | PAYMENT-REQUIRED header, resource.description |
| Payment header | X-PAYMENT | PAYMENT-SIGNATURE |
forge-feedback extension | Not available (v1 has no extensions) | Added |
accepts[].description before paying, and the feedback fields after. The browser paywall HTML is never modified.
Add Forge
import { paymentMiddleware } from "x402-express";
import { createForge } from "@forgeintel/sdk";
const forge = createForge({
apiKey: process.env.FORGE_API_KEY,
backendUrl: process.env.FORGE_BACKEND_URL,
publicUrl: process.env.PUBLIC_URL,
});
app.use(forge.middleware()); // before paymentMiddleware
app.use(paymentMiddleware(payTo, routes, { url: facilitatorUrl }));
Complete example
Fromexamples/express-x402-v1 in the Forge SDK repository. Forge lines are marked (forge).
server.ts
// An x402 v1 service (x402-express 1.x) wrapped by the Forge feedback SDK.
// v1 carries the 402 challenge in the JSON body, so Forge adds the rating sentence to each accepts[].description.
// The only Forge-specific lines are marked with (forge).
import express from "express";
import { paymentMiddleware } from "x402-express";
import { createForge } from "@forgeintel/sdk"; // (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") as `0x${string}`;
const NETWORK = (process.env.NETWORK ?? "base-sepolia") as "base" | "base-sepolia"; // v1 network names
const FACILITATOR_URL = (process.env.FACILITATOR_URL ?? "https://x402.org/facilitator") as `${string}://${string}`;
// (forge) PUBLIC_URL is this service's public origin; it's used in the rating URLs agents see.
const forge = createForge({
apiKey: required("FORGE_API_KEY"),
backendUrl: required("FORGE_BACKEND_URL"),
publicUrl: required("PUBLIC_URL"),
});
const app = express();
app.use(forge.middleware()); // (forge) before payment middleware
app.use(
paymentMiddleware(
PAY_TO,
{
"POST /api/text-stats": {
price: "$0.001",
network: NETWORK,
config: {
description: "Count characters, words and sentences in a text and estimate reading time.",
mimeType: "application/json",
},
},
},
{ url: FACILITATOR_URL },
),
);
app.get("/health", (_req, res) => void res.json({ status: "ok" }));
app.post("/api/text-stats", express.json({ limit: "100kb" }), (req, res) => {
const text = typeof req.body?.text === "string" ? req.body.text : "";
if (!text) return void res.status(400).json({ error: "text is required" });
const words = text.split(/\s+/).filter(Boolean).length;
res.json({
characters: text.length,
words,
sentences: text.split(/[.!?]+/).filter((s: string) => s.trim()).length,
reading_time_seconds: Math.ceil((words / 238) * 60),
});
});
const port = Number(process.env.PORT ?? 4021);
const server = app.listen(port, () => console.log(`x402 v1 example on http://localhost:${port}`));
process.on("SIGTERM", () => server.close(() => void forge.shutdown())); // (forge) flush pending events
export PAY_TO=0xYourAddress PUBLIC_URL=http://localhost:4021
export FORGE_API_KEY=forge_… FORGE_BACKEND_URL=https://your-forge-api/api/sdk/v2
npm start