Skip to main content

How to Implement a Crypto Paywall with x402 Payment Protocol

Updated on
May 21, 2025

9 min read

Overview

x402 is an open protocol for internet-native payments that leverages the HTTP 402 "Payment Required" status code. With x402, you can implement paywalls behind API requests, webpages, and more without the traditional friction of credit card processing, KYC, and high transaction fees.

In this guide, you'll utilize x402 as a seller and buyer on the Base Sepolia testnet via building a simple HTML/JS web app using Express as the backend to protect content behind a paywall and allow users to pay using cryptocurrency on the testnet.

Let's get started!

What You Will Do


  • Learn about x402 and how it works
  • Set up a HTML/JS web app with an Express server with x402 integration
  • Test the web app in a local environment with Base Sepolia

What You Will Need


  • Basic understanding of programming and blockchain concepts
  • Node.js installed (v22+)
  • An EVM-compatible wallet with ETH and USDC on Base Sepolia blockchain

What is x402?

x402 is a chain-agnostic protocol built around the HTTP 402 Payment Required status code that enables services to charge for access to their APIs and content directly over HTTP. This open payment standard allows clients to programmatically pay for resources without accounts, sessions, or credential management. With x402, any web service can require payment before serving a response, using crypto-native payments for speed and privacy.

Who is x402 for?


  • Sellers: Service providers who want to monetize their APIs or content. x402 enables direct, programmatic payments from clients with minimal setup.
  • Buyers: Developers and AI agents seeking to access paid services without accounts or manual payment flows.

Both sellers and buyers interact directly through HTTP requests, with payment handled transparently through the protocol.

Core Components of x402

Client/Server Architecture

Client Role (Buyer)


  • Initiates requests to access paid resources
  • Processes 402 responses and extracts payment details
  • Submits payment with the X-PAYMENT header

Server Role (Seller)


  • Defines payment requirements with HTTP 402 responses
  • Verifies incoming payment payloads
  • Provides the requested resource once payment confirms

Facilitators

Facilitators simplify the payment process by:


  • Verifying payment payloads
  • Settling payments on the blockchain for servers
  • Removing the need for servers to implement complex blockchain interactions

Having a Facilitator is optional, but it is recommended to use one. Currently, Coinbase Developer Platform (CDP) hosts the primary facilitator, offering fee-free USDC settlement on Base mainnet.

x402 Payment Flow

The x402 protocol leverages ERC-3009 TransferWithAuthorization standard to enable gasless transfers, a key component for frictionless web3 monetization. Let's cover the flow of using x402 along with specs to comply with the standard.


  1. Client requests a resource from a server
  2. Server responds with 402 Payment Required and payment instructions:
{
"maxAmountRequired": "0.10",
"resource": "/api/market-data",
"description": "Access requires payment",
"payTo": "0xABCDEF1234567890ABCDEF1234567890ABCDEF12",
"asset": "0xA0b86991C6218b36c1d19D4a2e9Eb0cE3606EB48",
"network": "ethereum-mainnet"
}

  1. Client prepares payment based on requirements
  2. Client retries with X-PAYMENT header containing signed payload
  3. Server verifies payment via facilitator
  4. Server settles payment on blockchain
  5. Server delivers resource once payment confirms

Now that we have a good understanding of what x402 is and how it works under the hood, let's move onto building a simple x402 demo. However, we will first need to fulfill some project prerequisites but if you already have ETH on Base Sepolia and an RPC endpoint, feel free to skip to the Project Set Up section.

Project Prerequisite: Retrieve USDC on Base Sepolia

You will need some USDC on Base Sepolia in order to demonstrate a simple x402 payment.

To retrieve USDC, navigate to the Circle USDC faucet and request some funds. Once you have USDC on Base Sepolia, you can move onto the next step.

Project Prerequisite: QuickNode Base Endpoint

To interact with Base Sepolia, you'll need an API endpoint to communicate with the network. You're welcome to use public nodes or deploy and manage your own infrastructure; however, if you'd like faster response times, you can leave the heavy lifting to us. Sign up for a free account here.

Once logged into QuickNode, click the Create an endpoint button, then select the Base chain and Sepolia network. After creating your endpoint, copy the HTTP Provider link and add it in your Web3 wallet's RPC settings as the Base Sepolia blockchain.

Ethereum Node Endpoint

Project Set Up

With the prerequisites out of the way, let's get into project setup. The demo we will be building in this guide will do the following:


  1. Allow a user to connect their wallet and sign a message that will be used to verify the payment
  2. The server will then authenticate the 402 status code was met and
  3. Respond with the paywalled API request or webpage

Let's get started!

Clone the Repository

Clone the qn-guide-examples repository:

git clone git@github.com:quiknode-labs/qn-guide-examples.git

Install Dependencies

Then, navigate inside the project folder and install dependencies:

cd sample-apps/coinbase-x402
npm install

Configure Environment

cp .env.local .env

Next, open up the .env file and ensure you put in the wallet address receiving payments and other environment variables:

# Receiving wallet address (where payments will be sent)
WALLET_ADDRESS=INPUT_WALLET_ADDRESS_HERE

# Environment
NODE_ENV=development

# Server port (default: 4021)
PORT=4021

Save the file.

Now, let's get into the logic of the codebase.

Server Setup

We will be using Express to create a simple server that will handle the payment and the API request.

Open the server.js file and let's recap the logic of the file.

import express from "express";
import { paymentMiddleware } from "x402-express";
import { facilitator } from "@coinbase/x402"; // For mainnet
import dotenv from "dotenv";
import path from "path";
import { log } from "./utils/log.js";
import { videoAccessHandler } from "./handlers/videoAccessHandler.js";

dotenv.config();

const app = express();

// Use Base Sepolia (testnet) for development
const network = "base-sepolia";
const facilitatorObj = { url: "https://x402.org/facilitator" };

// Serve static files from the public directory
app.use(express.static(path.join(process.cwd(), "public")));

app.use(express.json());

// x402 payment middleware configuration
app.use(
paymentMiddleware(
process.env.WALLET_ADDRESS, // your receiving wallet address
{
// Protected endpoint for authentication
"GET /authenticate": {
price: "$0.10", // Set your desired price
network: network,
},
},
facilitatorObj
)
);

// Add request logging middleware
app.use((req, res, next) => {
const start = Date.now();
log(`${req.method} ${req.url}`);
log(`Request Headers: ${JSON.stringify(req.headers)}`);
res.on("finish", () => {
const duration = Date.now() - start;
log(`${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
});
next();
});

// Authentication endpoint - just redirects to the authenticated content
app.get("/authenticate", (req, res) => {
log("Payment successful, redirecting to video content");
res.redirect("/video-content");
});

// Video content endpoint - serves the authenticated content
app.get("/video-content", videoAccessHandler);

// Serve the home page
app.get("/", (req, res) => {
res.sendFile(path.join(process.cwd(), "public", "index.html"));
});

export default app;

// This block runs the server locally
if (process.env.NODE_ENV !== "production") {
const PORT = process.env.PORT || 4021;
app.listen(PORT, () => {
console.log(`Server listening at http://localhost:${PORT}`);
});
}

Client Setup

Now we'll set up the client (our frontend), which we'll need to build three pages in the public directory of our project:


  • index.html: Homepage
  • authenticate.html: Authenticate and payment
  • video-content.html Page serving the paywalled video content

Let's dig into each. First, let's check out the homepage file (index.html):

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>x402 Video Paywall Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>x402 Video Paywall Demo</h1>
<p>Ready to access premium content with <a href="https://www.x402.org/" target="_blank">x402 payment</a> protocol.</p>
</header>

<div class="card">
<h3>Premium Video Content</h3>
<p>Access our premium video content for just $0.10 in USDC:</p>
<a href="/authenticate" class="cta-button">Pay $0.10 to Access Video</a>
</div>
</body>
</html>

The HTML above is the homepage of the app which users will be first shown.

Then, the payment page (i.e., authenticate.html):


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>x402 Video Paywall Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>x402 Video Paywall Demo</h1>
<p>Processing your payment...</p>
</header>

<div class="card">
<h3>Payment in Progress</h3>
<div class="payment-processing">
<div class="loader"></div>
<p>Your payment is being processed. Please do not close this window.</p>
<p>You will be automatically redirected to the video content once the payment is confirmed.</p>
</div>
<a href="/" class="cta-button secondary-button">Cancel Payment</a>
</div>
</body>
</html>

We'll also need to build the paywall webpage which won't be shown unless the user successfully submits payment and retrieves a successfull 402 status code response. This logic will be contained within the video-content.html file as shown below:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>x402 Video Paywall Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>x402 Video Paywall Demo</h1>
</header>

<div class="card">
<h3>Premium Video Content</h3>
<div class="video-container">
<iframe
width="100%"
height="450"
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
<br />
<p>You have successfully paid $0.10 in cryptocurrency to access this content.</p>
<a href="/" class="cta-button">Return to Homepage</a>
</div>
</body>
</html>

Helper Functions

We'll also need to create a helper function that processes the video page. This will be within the handlers/videoAccessHandler.js file:

import { log } from "../utils/log.js";
import path from "path";

export function videoAccessHandler(req, res) {
const startTime = Date.now();
try {
log("Processing video access request");

// Send the video content page
res.sendFile(path.join(process.cwd(), "public", "video-content.html"));

log(`Request completed in ${Date.now() - startTime}ms`);
} catch (error) {
log("Error serving video content:", "error", error);
res.status(500).send({
error: "Failed to serve video content",
message: error.message,
});
}
}

Testing the Paywall

With our backend and frontend setup, let's now try and test it. Start the server by running the command in your terminal window within the project's root folder:

node server.js

You'll see a page like this:

Homepage of x402 Web app

Click the payment button and you'll be forwarded to an authentication page (e.g., localhost:4021/authenticate) where you need to connect your wallet and pay.

Authenticate page of x402 Web app

You'll be prompted to sign a message such as the screenshot below. You'll notice we're signing a TransferWithAuthorization message (implemented in ERC-3009) which allows gasless transfers by delegating to a 3rd-party EOA or smart contract. The signer authorizes fund transfer from themself to another party.

Signed Message Window on MetaMask

This signed message contains all the necessary details for the transfer:


  • The sender address
  • The recipient address
  • The transfer amount
  • Validity time window
  • A unique nonce to prevent replay attacks

Once you sign the message in your wallet and the payment of $0.10 USDC is successful, you'll be redirected to the paywalled video page:

Authenticated paqge of the x402 Web app

More Use Cases


  • Content Monetization: Earn directly for individual articles, videos, or e-book chapters without subscription barriers.
  • Pay-As-You-Go Services: Pay exactly for what you use—whether API calls, cloud storage, or computing power—in precise increments.
  • Automated Service Payments: Let your software or smart devices handle small payments automatically for services they use.

Next Steps

To build on top of what you've just learned, try the following:


  • Deploy to a live server: Move beyond the local environment to host the demo on a real server accessible to others.
  • Customize payment amounts: Experiment with different price points or implement dynamic pricing based on content value.
  • Implement timed access: Create time-limited access where payments unlock content for specific durations.
  • Connect to Base Mainnet: Move from the Sepolia testnet to Base Mainnet for real-world implementation once you're ready.

Final Thoughts

That's it! You just learned how to use x402 to implement internet-native payments in your web application. By leveraging the HTTP 402 "Payment Required" status code and the blockchain, you've created a streamlined payment system that eliminates traditional payment processing friction.

If you are stuck or have questions, drop them in our Discord. Stay up to date with the latest by following us on Twitter (@QuickNode) or our Telegram announcement channel.

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide