Skip to main content
This guide shows how to accept x402 payments for your API endpoints using Semantic’s facilitator on Plasma or Stable. By the end you’ll have an Express server that gates routes behind USD₮ payments.
See a full working demo at github.com/SemanticPay/x402-usdt0-demo

Install

npm install @x402/express @x402/evm @x402/core express dotenv

How it works

Your server doesn’t handle payments directly. It delegates to Semantic’s facilitator:
  1. A buyer hits your endpoint without a payment header
  2. Your middleware responds with 402 Payment Required and the payment terms
  3. The buyer’s x402 client signs an EIP-3009 authorization and retries
  4. Your middleware forwards the signed payload to Semantic’s facilitator
  5. The facilitator verifies the signature, settles on-chain, and confirms
  6. Your route handler runs and returns the resource
You never touch private keys, gas tokens, or on-chain transactions. You just specify the price and the address to receive funds.

Pricing

The price field is a structured object that tells the buyer exactly what token to pay, how much, and on which chain. USDT0 uses 6 decimals, so "1000" = $0.001.
price: {
  amount: "1000",                                          // base units (6 decimals)
  asset: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",     // USDT0 contract
  extra: { name: "USDT0", version: "1", decimals: 6 },    // EIP-712 domain info
}
The extra fields are passed through to the buyer’s client for EIP-712 signature construction. name and version must match what the on-chain USDT0 contract expects.

USDT0 Deployments

Full deployment list at docs.usdt0.to.

Minimal server (single chain)

import { config } from "dotenv";
import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";

config();

// --- Config ---
const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`;
const FACILITATOR_URL = "https://x402.semanticpay.io/";
const PLASMA_NETWORK = "eip155:9745";
const USDT0_PLASMA = "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb";
const PRICE = "1000"; // $0.001 in base units

// --- Facilitator client ---
const facilitatorClient = new HTTPFacilitatorClient({ url: FACILITATOR_URL });

// --- Express app ---
const app = express();

app.use(
  paymentMiddleware(
    {
      "GET /weather": {
        accepts: [
          {
            scheme: "exact",
            network: PLASMA_NETWORK,
            price: {
              amount: PRICE,
              asset: USDT0_PLASMA,
              extra: { name: "USDT0", version: "1", decimals: 6 },
            },
            payTo: PAY_TO,
          },
        ],
        description: "Weather data",
        mimeType: "application/json",
      },
    },
    new x402ResourceServer(facilitatorClient).register(
      PLASMA_NETWORK,
      new ExactEvmScheme(),
    ),
  ),
);

app.get("/weather", (req, res) => {
  res.json({ weather: "sunny", temperature: 70 });
});

app.get("/health", (req, res) => {
  res.json({ status: "ok", chain: "plasma", payTo: PAY_TO });
});

const PORT = process.env.PORT || 4021;
app.listen(PORT, () => {
  console.log(`Server listening at http://localhost:${PORT}`);
  console.log(`Network: ${PLASMA_NETWORK}`);
  console.log(`USDT0: ${USDT0_PLASMA}`);
  console.log(`Pay to: ${PAY_TO}`);
});

Multi-chain server (Plasma + Stable)

Accept payments on both chains. The buyer’s client picks whichever network it has funds on.
import { config } from "dotenv";
import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";

config();

const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`;
const FACILITATOR_URL = "https://x402.semanticpay.io/";

// --- Network config ---
const NETWORKS = {
  plasma: {
    network: "eip155:9745" as const,
    usdt0: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
  },
  stable: {
    network: "eip155:988" as const,
    usdt0: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
  },
};

const PRICE = "1000"; // $0.001

function priceOnChain(chain: keyof typeof NETWORKS) {
  return {
    amount: PRICE,
    asset: NETWORKS[chain].usdt0,
    extra: { name: "USDT0", version: "1", decimals: 6 },
  };
}

// --- Facilitator + resource server ---
const facilitatorClient = new HTTPFacilitatorClient({ url: FACILITATOR_URL });

const resourceServer = new x402ResourceServer(facilitatorClient)
  .register(NETWORKS.plasma.network, new ExactEvmScheme())
  .register(NETWORKS.stable.network, new ExactEvmScheme());

// --- Express app ---
const app = express();

app.use(
  paymentMiddleware(
    {
      "GET /weather": {
        accepts: [
          {
            scheme: "exact",
            network: NETWORKS.plasma.network,
            price: priceOnChain("plasma"),
            payTo: PAY_TO,
          },
          {
            scheme: "exact",
            network: NETWORKS.stable.network,
            price: priceOnChain("stable"),
            payTo: PAY_TO,
          },
        ],
        description: "Weather data",
        mimeType: "application/json",
      },
    },
    resourceServer,
  ),
);

app.get("/weather", (req, res) => {
  res.json({ weather: "sunny", temperature: 70 });
});

app.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

const port = process.env.PORT || 4021;
app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});

Route configuration

The first argument to paymentMiddleware maps routes to payment requirements. The key format is METHOD /path.
paymentMiddleware(
  {
    "GET /api/data": {
      accepts: [
        {
          scheme: "exact",
          network: "eip155:9745",
          price: {
            amount: "10000",  // $0.01
            asset: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
            extra: { name: "USDT0", version: "1", decimals: 6 },
          },
          payTo: PAY_TO,
        },
      ],
      description: "Premium data feed",
      mimeType: "application/json",
    },
    "POST /api/generate": {
      accepts: [
        {
          scheme: "exact",
          network: "eip155:9745",
          price: {
            amount: "50000",  // $0.05
            asset: "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb",
            extra: { name: "USDT0", version: "1", decimals: 6 },
          },
          payTo: PAY_TO,
        },
      ],
      description: "AI generation endpoint",
      mimeType: "application/json",
    },
  },
  resourceServer,
);
Routes not listed in the config are not gated — they behave like normal Express routes. This is why /health works without payment in the examples above.

Lifecycle events

The Semantic facilitator supports an optional X-Event-Callback header on /verify and /settle requests. When provided, the facilitator POSTs real-time lifecycle events to that URL as verification and settlement happen. This is useful for building dashboards, logging pipelines, or payment flow visualizations. Events are fire-and-forget and do not block the facilitator’s response. If the callback URL is unreachable, events are silently dropped. If no header is provided, no events are sent.

Event types

TypeWhenKey fields
verify_startedFacilitator begins verifying the paymentdetails.network, details.checks
verify_completedVerification finisheddetails.isValid
verify_failedVerification threw an errordetails.error
settle_startedFacilitator is broadcasting the on-chain transactiondetails.network
settle_completedTransaction confirmed on-chaindetails.transactionHash
settle_failedSettlement threw an errordetails.error

Example: receiving events

Add a POST endpoint to your server:
app.post("/payment-events", (req, res) => {
  const { type, title, details } = req.body;
  console.log(`[${type}] ${title}`, details);
  res.json({ ok: true });
});
Then configure the facilitator client to include the callback header. Since HTTPFacilitatorClient from @x402/core doesn’t support custom headers directly, wrap fetch:
const CALLBACK_URL = "http://localhost:4021/payment-events";

const facilitatorClient = new HTTPFacilitatorClient({
  url: FACILITATOR_URL,
  fetch: (url, init) =>
    fetch(url, {
      ...init,
      headers: {
        ...init?.headers,
        "X-Event-Callback": CALLBACK_URL,
      },
    }),
});
With this in place, every /verify and /settle call to the facilitator will include the callback header, and your /payment-events endpoint will receive events like:
{
  "type": "settle_completed",
  "step": 10,
  "title": "Settlement Confirmed",
  "description": "Payment transaction confirmed on blockchain",
  "details": {
    "success": true,
    "transactionHash": "0xabc123...",
    "network": "eip155:9745"
  },
  "actor": "blockchain",
  "target": "facilitator"
}
For the full event reference, see the Facilitator API docs.

Environment variables

# .env
PAY_TO_ADDRESS=0xYourReceivingAddress
PORT=4021

Using with other frameworks

x402 also provides middleware for Hono and Next.js:
# Hono
npm install @x402/hono

# Next.js
npm install @x402/next
The pattern is the same: create a facilitator client, register the EVM scheme for your network(s), and apply middleware. See the x402 examples for framework-specific code.
Last modified on February 17, 2026