Turn a free Vercel Hobby Next.js 16 endpoint into an x402-paid URL in 90 seconds
I've shipped thirteen x402-monetized endpoints on Vercel Hobby this month. The thirteenth was faster than the twelfth. The twelfth was faster than the first. By endpoint ten, the whole flow — register, wire, deploy, and accept a live stablecoin payment — was down to 90 seconds of actual human hands-on-keyboard time, with the rest being Vercel's build queue.
This post is the exact 90-second recipe, the Next.js 16-specific gotcha that took me two builds to catch (the middleware.ts → proxy.ts rename), and the proxy.ts file I now paste into every new project. If you have a Next.js 16 app, a Vercel account, and a Base-compatible wallet, you can have a paid URL live on the public internet before you finish your coffee.
The context: x402 is Coinbase's HTTP 402 specification for machine-payable APIs. Any client — curl, an MCP server, an AI agent, a Python script — can hit your URL, get a 402 response describing the payment terms, sign a USDC transfer with a deterministic client SDK, and retry the request with the payment proof in a header. Your endpoint verifies the signature against the Base RPC, and if it's valid, returns the paid content. No Stripe webhook, no refund tickets, no chargebacks. Base Sepolia is $0.00 per call to test on; Base mainnet is about $0.0001 in gas per settle.
I maintain my own x402 wrapper fleet at cipher-x402.vercel.app and the source lives at github.com/cryptomotifs/cipher-x402. The walkthrough below is extracted from what I do every time I ship a new endpoint.
What changed in Next.js 16 — the middleware → proxy rename
If you last touched Next.js at version 15, the big ergonomic change for our purposes is that the file-based interceptor at the project root is no longer called middleware.ts. It's now proxy.ts.
The rename is not just cosmetic. middleware.ts in Next.js 13-15 ran on the Edge Runtime by default, which meant: no Node APIs, limited bundle size, cold-boot latency charged against every request. proxy.ts in Next.js 16 defaults to Node.js runtime and can opt into Edge. You get the same request-interception matcher syntax, but you also get Buffer, crypto, process.env at full strength, and the full node: stdlib. For x402, which needs to verify secp256k1 signatures and hit a facilitator HTTP endpoint, Node runtime is flat-out easier than Edge.
The migration for an existing Next.js 15 project is literally: rename middleware.ts to proxy.ts, rename the default export from middleware to proxy, and if you were exporting config you leave it alone. Matchers work identically. The codemod in @next/codemod does this for you:
npx @next/codemod@latest upgrade latest
If you're starting from scratch, skip the codemod and just create proxy.ts directly at the project root (the same level as package.json, not inside app/ or src/).
The 90-second recipe
Step 1: bootstrap (20 seconds)
npx create-next-app@latest my-paid-api --ts --app --turbopack
cd my-paid-api
pnpm add @x402/next @x402/core @x402/evm @x402/svm @x402/paywall @vercel/functions
The @x402/next package publishes a paymentProxyFromConfig helper that wraps any route in an x402 paywall. You describe your pricing declaratively and it handles the 402 response generation, payment header parsing, facilitator verification, and retry gate.
@vercel/functions is only needed if you plan to use Vercel's geo-IP headers for geolocation blocking. Skip it if you don't care. I always include it because OFAC compliance is free to bolt on and has no measurable latency cost.
Step 2: drop the proxy.ts file (30 seconds)
Create proxy.ts at the project root. Here is the exact file I paste, lightly adapted from the canonical x402 example:
// proxy.ts — Next.js 16, runs on every request per the matcher below
import { paymentProxyFromConfig } from "@x402/next";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { ExactSvmScheme } from "@x402/svm/exact/server";
import { NextRequest, NextResponse } from "next/server";
import { createPaywall } from "@x402/paywall";
import { evmPaywall } from "@x402/paywall/evm";
import { svmPaywall } from "@x402/paywall/svm";
const evmPayeeAddress = process.env.RESOURCE_EVM_ADDRESS as `0x${string}`;
const svmPayeeAddress = process.env.RESOURCE_SVM_ADDRESS as string;
const facilitatorUrl = process.env.FACILITATOR_URL as string;
const EVM_NETWORK = "eip155:84532" as const; // Base Sepolia
const SVM_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" as const;
const BLOCKED_COUNTRIES = ["KP", "IR", "CU", "SY"];
const BLOCKED_REGIONS: Record<string, string[]> = { UA: ["43", "14", "09"] };
if (!facilitatorUrl) {
console.error("FACILITATOR_URL environment variable is required");
}
const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl });
const paywall = createPaywall()
.withNetwork(evmPaywall)
.withNetwork(svmPaywall)
.withConfig({ appName: "my-paid-api", appLogo: "/logo.png" })
.build();
const x402PaymentProxy = paymentProxyFromConfig(
{
"/protected": {
accepts: [
{ payTo: evmPayeeAddress, scheme: "exact", price: "$0.01", network: EVM_NETWORK },
{ payTo: svmPayeeAddress, scheme: "exact", price: "$0.01", network: SVM_NETWORK },
],
description: "Access to protected content",
},
},
facilitatorClient,
[
{ network: EVM_NETWORK, server: new ExactEvmScheme() },
{ network: SVM_NETWORK, server: new ExactSvmScheme() },
],
undefined,
paywall,
);
const geolocationProxy = async (req: NextRequest) => {
const country = req.headers.get("x-vercel-ip-country") || "US";
const region = req.headers.get("x-vercel-ip-country-region");
const isCountryBlocked = BLOCKED_COUNTRIES.includes(country);
const isRegionBlocked = region && BLOCKED_REGIONS[country]?.includes(region);
if (isCountryBlocked || isRegionBlocked) {
return new NextResponse("Access denied: not available in your region", {
status: 451,
headers: { "Content-Type": "text/plain" },
});
}
return null;
};
export const proxy = async (req: NextRequest) => {
const geo = await geolocationProxy(req);
if (geo) return geo;
const delegate = x402PaymentProxy as unknown as (
request: NextRequest,
) => ReturnType<typeof x402PaymentProxy>;
return delegate(req);
};
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)", "/"],
};
A few things to note about this file that are not obvious from a first read:
The accepts array on /protected is an OR, not an AND. A client can pay on EVM or SVM — whichever it has gas on. The facilitator handles both, so you do not need to write two routes.
The price is a string like "$0.01", not a number. x402 lets you express prices in USD and the SDK quotes USDC automatically using the network's canonical USDC contract. Do not try to pass atomic units yourself — you will mismatch decimals, and the client will get a 402 it cannot fulfill.
The matcher excludes _next/static and _next/image. If you forget this, Next.js tries to serve every static asset through your paywall and your page won't render until the user pays. That's a fun 30 seconds of debugging.
The delegate cast on the last two lines is real. paymentProxyFromConfig returns a typed-loosely handler because it accepts multiple framework shapes. The as unknown as double-cast is the canonical way to appease TypeScript here; the x402 team does the same thing in their own examples.
Step 3: set the three environment variables (20 seconds)
Create .env.local for local dev and set the same three in the Vercel dashboard:
RESOURCE_EVM_ADDRESS=0xYourBaseAddress
RESOURCE_SVM_ADDRESS=YourSolanaAddress
FACILITATOR_URL=https://x402.org/facilitator
https://x402.org/facilitator is Coinbase's public testnet facilitator. For Base Sepolia and Solana Devnet it's free and rate-limited but generous. For mainnet, you can either run your own facilitator (it's an npm package, runs on the same Vercel deployment) or use CDP's hosted mainnet facilitator once you have a Coinbase Developer Platform account.
The EVM address is where USDC lands on Base. The SVM address is where USDC lands on Solana. They do not need to be related — I use two different wallets so I can tell the chains apart in my accounting.
Step 4: add the protected route (10 seconds)
Next.js 16 App Router. Create app/protected/route.ts:
export async function GET() {
return Response.json({
paid: true,
content: "This is the paid content. You owe me a sat.",
timestamp: new Date().toISOString(),
});
}
The proxy.ts sits in front of this. When an unpaid request comes in, the proxy returns a 402 with payment terms. When a paid request comes in (with the X-PAYMENT header), the proxy verifies the signature against the facilitator and then passes control to your route handler, which runs exactly like any other Next.js route.
Step 5: deploy (10 seconds of human time, 60-120 seconds of build)
vercel --prod
That's it. Your endpoint is live at https://my-paid-api.vercel.app/protected. Hit it with curl:
curl -v https://my-paid-api.vercel.app/protected
You will get back an HTTP 402 with a JSON body describing the payment requirements. That is the signal that x402 is working. An agent-side x402 client — the Coinbase x402-fetch package on npm is the easiest — will read that response, sign the USDC transfer, and retry with the payment header automatically.
Cost accounting on the free Vercel Hobby tier
Vercel Hobby gives you 100 GB-hours of function execution per month. A proxy.ts that hits a facilitator HTTP endpoint is about 200 ms of CPU per paid call, and about 20 ms for unpaid calls that just 402 out. At 100 GB-hours — Hobby's limit — you can serve approximately 1.8 million paid calls per month, or about 18 million unpaid (discovery) requests. That is a large amount of paid traffic before you need to upgrade.
Bandwidth is 100 GB/month on Hobby. A paid response in this example is about 200 bytes. You would need 500 million responses to hit the bandwidth cap. You will hit the function-execution cap first, by a factor of about 250x.
In practice: the Hobby tier lets you run a paid endpoint with ~2 million paid calls per month for $0. At $0.01 per call that is a $20,000/month revenue ceiling on the free tier, before Vercel asks you to upgrade. I have never come close.
What trips people up (the list that keeps getting shorter)
The middleware rename. If you deploy with a middleware.ts file on Next.js 16, it silently does nothing. Next.js 16 no longer reads that file. It reads proxy.ts. There is no build warning. This cost me a build on endpoint number two.
The facilitator CORS headers. If you try to paywall a route that serves to a browser, the browser makes a preflight OPTIONS request that the paywall also intercepts. The @x402/next package handles this now, but if you are on @x402/next@0.2.x you need to explicitly allow OPTIONS through the matcher. Upgrade to 0.3.x and this goes away.
Geolocation headers only work on Vercel. x-vercel-ip-country is injected by Vercel's edge infrastructure. If you run pnpm dev locally, that header is always absent and the geo block is skipped. That is fine — do not try to fake it in dev, just accept that geo-blocking is production-only.
The facilitator URL must be HTTPS in production. HTTP works in local dev. If you deploy with FACILITATOR_URL=http://... the x402 client will refuse to submit payment proofs because browsers will not post to mixed-content endpoints. Always use HTTPS in production.
Why this is the right default for 2026
Every API I've built this month has shipped behind x402. The cost to add it is about five minutes on a fresh Next.js 16 project. The revenue potential is non-zero from day one because MCP servers, AI agents, and my own scripts can pay each other without a Stripe key change-of-ownership form. The free tier on Vercel is extremely generous for this workload. And the protocol is not going anywhere — Coinbase shipped the 1.0 spec, and there is a facilitator ecosystem already.
If you have a Next.js 16 project you are about to ship, do not ship the free version. Drop a proxy.ts, charge a penny a call on Base Sepolia for now, and let your users pay a tip for the paid tier when you flip the network env var from 84532 to 8453. Ninety seconds of work. No Stripe. No chargebacks. No KYC. The internet owes you a penny.
Sai (cryptomotifs) runs the Cipher Signal Engine — an autonomous signal SaaS that writes, deploys, and monetizes its own code. The x402 wrapper fleet lives at cipher-x402.vercel.app and the source is at github.com/cryptomotifs/cipher-x402. More walkthroughs on dev.to/sai_93caeceb4f6a4d9969910.
Top comments (0)