create-eth
Version:
Create a Scaffold-ETH-2 app
231 lines (174 loc) • 9.06 kB
Markdown
---
name: x402
description: "Add x402 payment-gated routes to a Scaffold-ETH 2 project. Use when the user wants to: monetize an API with micropayments, add HTTP 402 payment required to routes, gate content behind crypto payments, implement pay-per-call APIs, or integrate the x402 protocol."
---
Check if `./packages/nextjs/scaffold.config.ts` exists directly in the current working directory (do not search subdirectories). If it doesn't exist, this is not a Scaffold-ETH 2 project. Follow the instructions at https://docs.scaffoldeth.io/SKILL.md to scaffold it first. If it exists, continue directly with building.
## Overview
[x402](https://www.x402.org/) is an open payment protocol by Coinbase that uses HTTP status code 402 ("Payment Required") to enable instant stablecoin micropayments over HTTP. When a client requests a protected resource without payment, the server responds with 402 + payment instructions. The client signs a payment, retries the request, and gets access.
This skill covers integrating x402 into SE-2 using Next.js middleware. For the full protocol spec and advanced usage, refer to the [x402 docs](https://docs.cdp.coinbase.com/x402/welcome) or the [GitHub repo](https://github.com/coinbase/x402). This skill focuses on SE-2 integration specifics and gotchas.
## Dependencies
### NextJS package
Add to `packages/nextjs/package.json`:
```json
{
"dependencies": {
"@x402/core": "^2.2.0",
"@x402/evm": "^2.2.0",
"@x402/next": "^2.2.0",
"@x402/paywall": "^2.2.0"
}
}
```
If the user wants a CLI script to test API routes programmatically, add to `packages/hardhat/package.json`:
```json
{
"scripts": {
"send402request": "hardhat run scripts/send402request.ts"
},
"dependencies": {
"@x402/core": "^2.2.0",
"@x402/evm": "^2.2.0",
"@x402/fetch": "^2.2.0"
}
}
```
Add to root `package.json`: `"send402request": "yarn workspace @se-2/hardhat send402request"`
Create `packages/nextjs/.env.development` (or `.env.local`):
```env
NEXT_PUBLIC_FACILITATOR_URL=https://x402.org/facilitator
RESOURCE_WALLET_ADDRESS=0xYourAddressHere
NETWORK=eip155:84532
```
x402 payments happen onchain, so `targetNetworks` must include a supported chain. For development, use `baseSepolia`:
```typescript
targetNetworks: [chains.baseSepolia],
```
**Do not use `hardhat` (localhost) as the target network for x402** — the facilitator needs a real chain to verify/settle payments.
```
Client GET /api/protected
→ Server: no X-PAYMENT header → responds 402 + PAYMENT-REQUIRED header
Client: signs EIP-712 payment authorization (USDC approve)
Client GET /api/protected + X-PAYMENT header
→ Server middleware: sends payment to facilitator for verification
→ Facilitator: verifies signature, checks balance
→ Server: serves content
→ Server middleware: sends settlement to facilitator
→ Facilitator: executes the USDC transfer onchain
```
Key insight: **The user never sends a transaction themselves.** They sign an EIP-712 message authorizing a USDC transfer. The facilitator executes it after the server confirms content was delivered.
## Middleware Configuration
The core of x402 integration is `middleware.ts` in the Next.js app root. The v2 API uses `paymentProxy` from `@x402/next` with explicit server and paywall setup.
```typescript
// packages/nextjs/middleware.ts
import { paymentProxy } from "@x402/next";
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
import { registerExactEvmScheme } from "@x402/evm/exact/server";
import { createPaywall } from "@x402/paywall";
import { evmPaywall } from "@x402/paywall/evm";
const facilitatorUrl = process.env.NEXT_PUBLIC_FACILITATOR_URL!;
const payTo = process.env.RESOURCE_WALLET_ADDRESS as `0x${string}`;
const network = process.env.NETWORK as `${string}:${string}`;
// Create facilitator client and resource server
const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl });
const server = new x402ResourceServer(facilitatorClient);
registerExactEvmScheme(server);
// Create paywall UI (shown to browsers visiting protected pages)
const paywall = createPaywall()
.withNetwork(evmPaywall)
.withConfig({
appName: "My dApp",
appLogo: "/logo.png",
testnet: true, // set false for mainnet
})
.build();
export const middleware = paymentProxy(
{
"/api/payment/:path*": {
accepts: [
{
scheme: "exact",
price: "$0.01",
network,
payTo,
},
],
description: "Access to premium API data",
mimeType: "application/json",
},
"/payment/:path*": {
accepts: [
{
scheme: "exact",
price: "$0.01",
network,
payTo,
},
],
description: "Access to premium content",
mimeType: "text/html",
},
},
server,
undefined, // optional request context
paywall,
);
// IMPORTANT: matcher must cover all protected routes
export const config = {
matcher: ["/api/payment/:path*", "/payment/:path*"],
};
```
The v2 API uses [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) network identifiers — format: `eip155:{chainId}` for EVM chains.
| CAIP-2 ID | Chain | Notes |
|-----------|-------|-------|
| `eip155:84532` | Base Sepolia | Default for development — [Circle faucet](https://faucet.circle.com/) for test USDC |
| `eip155:8453` | Base | Recommended for production — lowest fees |
Legacy network names (`base-sepolia`, `base`, etc.) may still work for backwards compatibility, but prefer CAIP-2 format. For the full list of supported networks, check the [x402 docs](https://docs.cdp.coinbase.com/x402/welcome).
## Gotchas & Common Pitfalls
**Facilitator is required.** x402 doesn't do peer-to-peer payments. The facilitator service verifies signatures and executes settlements. For testnet, `https://x402.org/facilitator` works without signup. For production, you may need to run your own — check [x402 docs](https://docs.cdp.coinbase.com/x402/welcome).
**Register the EVM scheme.** The server needs `registerExactEvmScheme(server)` in middleware.ts. Without this, payment payloads won't be understood.
**Payments are in USDC by default.** The `$0.01` price syntax means USDC.
**Don't use `hardhat` localhost as the network.** The facilitator can't verify or settle payments on a local chain. Always use a testnet (`eip155:84532`) even during development.
**The `matcher` in `middleware.ts` must cover protected routes.** If you add a new protected route in the routes config but forget to add it to `matcher`, the middleware won't run on that route.
## CLI Payment Script
For testing API routes programmatically (without a browser), create a script using `@x402/fetch`:
```typescript
// packages/hardhat/scripts/send402request.ts
import { privateKeyToAccount } from "viem/accounts";
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
async function main() {
const privateKey = process.env.DEPLOYER_PRIVATE_KEY as `0x${string}`;
if (!privateKey) { console.log("No deployer key. Run `yarn generate` first."); return; }
const signer = privateKeyToAccount(privateKey);
const client = new x402Client();
registerExactEvmScheme(client, { signer });
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const response = await fetchWithPayment("http://localhost:3000/api/payment/builder", { method: "GET" });
console.log("Response:", await response.json());
}
main().catch(console.error);
```
> **Note:** Register the EVM scheme on the client side too — `registerExactEvmScheme(client, { signer })` from `@x402/evm/exact/client`.
1. Set `targetNetworks: [chains.baseSepolia]` in `scaffold.config.ts`
2. Configure `.env.development` with facilitator URL, pay-to address, and `NETWORK=eip155:84532`
3. `yarn start` — visit `http://localhost:3000`
4. Navigate to a protected page — you should see the x402 paywall
5. To test API routes: `curl http://localhost:3000/api/payment/builder` should return 402 with `PAYMENT-REQUIRED` header
6. To test paid access: `yarn send402request` (needs funded wallet on Base Sepolia — get test USDC from [Circle faucet](https://faucet.circle.com/))
- Switch `NETWORK` to `eip155:8453` (Base mainnet)
- Update `scaffold.config.ts` to target the mainnet chain
- Set `RESOURCE_WALLET_ADDRESS` to your production payment receiver
- Set `testnet: false` in paywall config