Skip to main content
In this quickstart, you will add Circle Gateway payment middleware to an Express API so that it accepts gasless USDC payments via the x402 protocol. By the end, your API will return 402 Payment Required for unpaid requests and serve resources when a valid payment signature is provided.

Prerequisites

Before you begin, ensure you have:
  • Node.js v18 or later and npm installed.
  • An EVM wallet address where you want to receive USDC payments.

Step 1: Set up your project

1.1. Create a new project

Create a new directory and initialize a Node.js project:
mkdir nanopayments-seller
cd nanopayments-seller
npm init -y

npm pkg set type=module
npm pkg set scripts.start="tsx server.ts"

1.2. Install dependencies

npm install @circlefin/x402-batching express tsx typescript
npm install --save-dev @types/node @types/express

1.3. Initialize TypeScript

This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:
npx tsc --init
Then, update the tsconfig.json file:
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF

Step 2: Create the server

Create a new file server.ts with an Express app and the Gateway middleware:
server.ts
import express from "express";
import { createGatewayMiddleware } from "@circlefin/x402-batching/server";

const app = express();

const gateway = createGatewayMiddleware({
  sellerAddress: "0xYOUR_WALLET_ADDRESS",
});
Replace 0xYOUR_WALLET_ADDRESS with the EVM address where you want to receive payments. By default, the middleware accepts payments from all Gateway-supported blockchains.

Step 3: Protect a route

Use gateway.require() to protect any route with a price. When a request arrives without a valid payment, the middleware returns 402 Payment Required with the payment details. When a valid payment signature is attached, the middleware verifies it, settles it with Gateway, and calls next():
server.ts
app.get("/premium-data", gateway.require("$0.01"), (req, res) => {
  const { payer, amount, network } = req.payment!;
  console.log(`Paid ${amount} USDC by ${payer} on ${network}`);

  res.json({
    secret: "The treasure is hidden under the doormat.",
    paid_by: payer,
  });
});

app.listen(3000, () => {
  console.log("Server listening at http://localhost:3000");
});

Step 4: Test the server

4.1. Start the server

npm start

4.2. Send an unpaid request

In a separate terminal, use curl to verify the server returns a 402 response:
curl -i http://localhost:3000/premium-data
You should see a 402 Payment Required response with a JSON body containing the accepts array with payment options.

4.3. Pay with a buyer client

Use the buyer quickstart client to make a gasless payment to your server:
import { GatewayClient } from "@circlefin/x402-batching/client";

const client = new GatewayClient({
  chain: "arcTestnet",
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
});

const { data, status } = await client.pay("http://localhost:3000/premium-data");
console.log(`Status: ${status}`);
console.log("Data:", data);
If the payment succeeds, you’ll see the JSON response from your protected endpoint.

Advanced: Use BatchFacilitatorClient directly

If you are not using Express, or need custom logic like dynamic pricing, use the BatchFacilitatorClient directly:
server.ts
import { BatchFacilitatorClient } from "@circlefin/x402-batching/server";

const facilitator = new BatchFacilitatorClient();

async function handleRequest(req, res) {
  const signature = req.headers["PAYMENT-SIGNATURE"];

  if (!signature) {
    return res.status(402).json({
      x402Version: 1,
      accepts: [
        {
          scheme: "GatewayWalletBatched",
          amount: "10000", // 0.01 USDC (6 decimals)
          payTo: "0xYOUR_ADDRESS",
        },
      ],
    });
  }

  const payload = JSON.parse(Buffer.from(signature, "base64").toString());
  const requirements = {
    /* your payment requirements */
  };

  const settlement = await facilitator.settle(payload, requirements);
  if (!settlement.success) {
    return res.status(402).json({ error: "Settlement failed" });
  }

  res.json({ data: "Your paid content" });
}
Gateway’s settle() endpoint is optimized for low latency and guarantees settlement. Use settle() directly rather than calling verify() followed by settle() in production flows.

Limit accepted networks (optional)

By default, the middleware accepts payments from any Gateway-supported blockchain. This maximizes your reach since any buyer with a Gateway balance can pay you, regardless of which blockchain they deposited on. If you need to restrict payments to specific networks:
const gateway = createGatewayMiddleware({
  sellerAddress: "0x...",
  networks: ["eip155:5042002"], // Only accept Arc Testnet
});
Payment signatures must have at least 3 days of validity. The validBefore timestamp in the buyer’s EIP-3009 authorization must be at least 3 days in the future, or Gateway will reject it.