skills/algorand-devrel/algorand-agent-skills/create-typescript-x402-paywall

create-typescript-x402-paywall

SKILL.md

Creating Paywall UIs for x402-Protected Endpoints

Build server-side middleware that serves payment HTML pages to browsers and JSON 402 responses to API clients, with automatic wallet integration for Algorand (Pera, Defly, Lute).

Prerequisites

Before using this skill, ensure:

  1. A backend framework (Express.js, Hono, or Next.js)
  2. A facilitator service running and accessible via URL
  3. An Algorand address to receive payments (payTo address)

Core Workflow: Server-Side Paywall Architecture

The paywall system has two sides. The server middleware detects missing payments and serves either a JSON 402 or an HTML paywall page. The HTML page handles wallet connection, signing, and retry:

Browser Request
     |
     v
[Server Middleware]
     |
     +-- Has PAYMENT-SIGNATURE header?
     |     |
     |     +-- Yes: Verify payment, settle transaction, return content
     |     +-- No: Is this a browser request (Accept: text/html)?
     |           |
     |           +-- Yes: Return paywall HTML page (402)
     |           +-- No: Return JSON 402 response
     |
     v
[Paywall HTML Page]
     |
     +-- Reads window.x402 config
     +-- Shows wallet connection UI (Pera/Defly/Lute)
     +-- User connects wallet and approves payment
     +-- Retries request with PAYMENT-SIGNATURE header
     +-- Redirects to paid content

How to Proceed

Step 1: Install Dependencies

Server-side packages:

npm install @x402-avm/paywall @x402-avm/avm @x402-avm/core

Plus your framework middleware:

# Express.js
npm install @x402-avm/express

# Hono
npm install @x402-avm/hono

# Next.js
npm install @x402-avm/next

Step 2: Create the Paywall with PaywallBuilder

The PaywallBuilder creates a PaywallProvider that generates HTML paywall pages. Register network handlers in priority order -- first match wins:

import { createPaywall, avmPaywall } from "@x402-avm/paywall";

const paywall = createPaywall()
  .withNetwork(avmPaywall)    // Supports algorand:* networks
  .withConfig({
    appName: "My Premium API",
    appLogo: "https://example.com/logo.png",
    testnet: true,
  })
  .build();

For multi-network support, register multiple handlers:

import { createPaywall, avmPaywall, evmPaywall, svmPaywall } from "@x402-avm/paywall";

const paywall = createPaywall()
  .withNetwork(avmPaywall)    // algorand:*
  .withNetwork(evmPaywall)    // eip155:*
  .withNetwork(svmPaywall)    // solana:*
  .withConfig({ appName: "Universal Paywall", testnet: true })
  .build();

Step 3: Define Protected Routes

Routes specify what payment is required for each endpoint:

const routes = {
  "/api/premium-content": {
    accepts: {
      scheme: "exact",
      network: "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
      asset: "10458941",           // USDC ASA ID on testnet
      payTo: "YOUR_ALGORAND_ADDRESS_HERE",
      price: "$0.01",             // 0.01 USDC
      maxTimeoutSeconds: 300,
    },
    description: "Access to premium content",
    mimeType: "application/json",
  },
};

Step 4: Apply Middleware (Express.js)

import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402-avm/express";

const app = express();
const server = new x402ResourceServer({ url: process.env.FACILITATOR_URL! });

app.use(paymentMiddleware(routes, server, { testnet: true }, paywall));

app.get("/api/premium-content", (req, res) => {
  res.json({ content: "This is the paid content." });
});

app.listen(3000);

Step 4 (Alt): Apply Middleware (Hono)

import { Hono } from "hono";
import { paymentMiddleware, x402ResourceServer } from "@x402-avm/hono";

const app = new Hono();
const server = new x402ResourceServer({ url: process.env.FACILITATOR_URL! });

app.use("*", paymentMiddleware(routes, server, { testnet: true }, paywall));

app.get("/api/premium-content", (c) => {
  return c.json({ content: "Premium content unlocked!" });
});

export default app;

Step 4 (Alt): Apply Middleware (Next.js)

Middleware approach:

// middleware.ts
import { paymentProxy, x402ResourceServer } from "@x402-avm/next";

const server = new x402ResourceServer({ url: process.env.FACILITATOR_URL! });
const proxy = paymentProxy(routes, server, { testnet: true }, paywall);

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/api/premium")) {
    return proxy(request);
  }
  return NextResponse.next();
}

Route handler approach (withX402):

// app/api/premium/route.ts
import { withX402, x402ResourceServer } from "@x402-avm/next";

const server = new x402ResourceServer({ url: process.env.FACILITATOR_URL! });

async function handler(request: NextRequest) {
  return NextResponse.json({ content: "Premium content!" });
}

export const GET = withX402(handler, routeConfig, server, { testnet: true }, paywall);

Important Rules / Guidelines

  1. Handler order matters -- withNetwork() calls determine priority. The first handler whose supports() returns true for a payment requirement is used
  2. avmPaywall supports algorand:* -- Any network starting with algorand: is matched
  3. Testnet vs Mainnet -- Set testnet: true/false in both PaywallConfig and middleware config
  4. USDC ASA IDs -- Testnet: 10458941, Mainnet: 31566704
  5. Facilitator URL is required -- The middleware needs a running facilitator to verify and settle payments
  6. Tree-shaking -- Import only the network handlers you need. avmPaywall can be imported from @x402-avm/paywall or @x402-avm/paywall/avm
  7. Multiple routes -- Define multiple entries in the routes object, each with its own price, description, and asset

Wallet Integration

The AVM paywall HTML page uses @wallet-standard/app to discover Algorand wallets:

Wallet Type Feature
Pera Wallet Mobile + WalletConnect algorand:signTransaction
Defly Wallet Mobile + WalletConnect algorand:signTransaction
Lute Wallet Browser Extension algorand:signTransaction

The paywall page automatically:

  • Discovers available wallets via wallet-standard
  • Shows a wallet selection UI
  • Checks USDC balance for the connected account
  • Handles transaction signing
  • Retries the original request with the payment header
  • Redirects to the paid content on success

Multi-Network Paywalls

Accept payments on multiple chains by specifying an array in accepts:

const routes = {
  "/api/premium": {
    accepts: [
      {
        scheme: "exact",
        network: "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
        asset: "10458941",
        payTo: "ALGO_ADDRESS_HERE",
        price: "$0.01",
        maxTimeoutSeconds: 300,
      },
      {
        scheme: "exact",
        network: "eip155:84532",
        asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
        payTo: "0xEVM_ADDRESS_HERE",
        price: "$0.01",
        maxTimeoutSeconds: 30,
      },
    ],
    description: "Premium content - pay with USDC on Algorand or Base",
    mimeType: "application/json",
  },
};

Custom Paywall Handler

If the built-in paywall does not meet your needs, implement a custom PaywallNetworkHandler:

import type { PaywallNetworkHandler } from "@x402-avm/paywall";

const customHandler: PaywallNetworkHandler = {
  supports(requirement) {
    return requirement.network.startsWith("algorand:");
  },
  generateHtml(requirement, paymentRequired, config) {
    return `<!DOCTYPE html>
      <html><body>
        <h1>Pay ${requirement.amount} to access</h1>
        <script>window.x402 = ${JSON.stringify({ paymentRequired, ...config })};</script>
      </body></html>`;
  },
};

const paywall = createPaywall()
  .withNetwork(customHandler)
  .build();

Common Errors / Troubleshooting

Error Cause Solution
Paywall not shown in browser Middleware not applied or route not matched Check route patterns match request paths
JSON 402 returned to browser Browser not sending Accept: text/html Ensure direct browser navigation, not programmatic fetch
Wallet not detected Wallet extension not installed Install Pera, Defly, or Lute wallet
"Insufficient balance" Account has no USDC Fund the wallet with USDC on the correct network
Facilitator unreachable Wrong URL or service down Verify FACILITATOR_URL environment variable
Payment not settling Facilitator signer not funded Ensure the facilitator address has ALGO for fees

References / Further Reading

Weekly Installs
2
GitHub Stars
26
First Seen
Feb 12, 2026
Installed on
gemini-cli2
antigravity2
claude-code2
github-copilot2
continue1
mcpjam1