Skip to main content
Accept crypto payments in your Express API with a single middleware function.

Installation

# npm
npm install @armory-sh/middleware-express

# yarn
yarn add @armory-sh/middleware-express

# pnpm
pnpm add @armory-sh/middleware-express

# bun
bun add @armory-sh/middleware-express

Quick Start

This page covers Express v5+ with @armory-sh/middleware-express. For Express v4, use the separate package page: @armory-sh/middleware-express-v4.
import express from 'express';
import { paymentMiddleware } from '@armory-sh/middleware-express';

const app = express();

app.use(paymentMiddleware({
  requirements: {
    to: '0xYourWalletAddress...',
    amount: 1000000n, // 1 USDC (6 decimals)
    expiry: Date.now() + 3600 * 1000,
    chainId: 'eip155:8453',
    assetId: 'eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71B54bdA02913'
  }
}));

app.get('/api/protected', (req, res) => {
  // Payment info is attached to the request
  console.log(req.payment);
  res.json({ message: 'Payment verified!' });
});

app.listen(3000);

How It Works

The middleware intercepts requests and:
  1. Checks for payment headers (X-Payment or X402-PAYMENT)
  2. Verifies the payment signature and amount
  3. Attaches payment info to req.payment
  4. Calls next() if valid, or returns 402 if invalid

Configuration

Payment Requirements

{
  requirements: {
    to: string,           // Your wallet address
    amount: bigint,       // Amount in smallest unit (wei for native, token decimals for ERC20)
    expiry: number,       // Expiry timestamp (milliseconds)
    chainId: string,      // CAIP-2 chain ID (e.g., 'eip155:8453')
    assetId: string       // CAIP-19 asset ID
  }
}

Common Token Addresses

NetworkTokenAddress
BaseUSDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
EthereumUSDC0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
BaseUSDT0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb

Verification Options

See runtime verification configuration in your deployment environment and middleware setup.

Advanced Mix-and-Match Config (Current API)

import express from "express";
import { paymentMiddleware } from "@armory-sh/middleware-express";

const app = express();

app.use(
  "/api/premium/*",
  paymentMiddleware({
    payTo: "0x1234567890123456789012345678901234567890",
    chains: ["base", "skale-base"],
    tokens: ["usdc", "usdt", "weth", "wbtc"],
    amount: "1.0",
    facilitatorUrl: "https://fallback-facilitator.example",
    facilitatorUrlByChain: {
      base: "https://payai.example",
    },
    facilitatorUrlByToken: {
      base: {
        usdc: "https://payai.example",
      },
      "skale-base": {
        usdc: "https://payai.example",
        usdt: "https://kobaru.example",
        weth: "https://kobaru.example",
        wbtc: "https://kobaru.example",
      },
    },
    extensions: {
      bazaar: { info: { input: { sku: "pro-plan" } }, schema: { type: "object" } },
      "sign-in-with-x": { info: { domain: "api.example.com" }, schema: { type: "object" } },
    },
  }),
);
You can mix and match network, token, facilitator, and amount by providing explicit requirements:
app.use(
  "/api/checkout",
  paymentMiddleware({
    requirements: [
      {
        scheme: "exact",
        network: "eip155:8453",
        amount: "1000000",
        asset: "0x833589fCD6eDb6E08f4c7C32D4f71B54bdA02913",
        payTo: "0x1234567890123456789012345678901234567890",
        maxTimeoutSeconds: 300,
      },
      {
        scheme: "exact",
        network: "eip155:1187947933",
        amount: "2500000",
        asset: "0x5f9beea3f6f22be1f4f8efc120f5095f58dbb8a2",
        payTo: "0x1234567890123456789012345678901234567890",
        maxTimeoutSeconds: 300,
      },
    ],
    facilitatorUrlByToken: {
      base: { usdc: "https://payai.example" },
      "skale-base": { usdt: "https://kobaru.example" },
    },
  }),
);
extensions are fail-open filtered per facilitator capability (/supported): unsupported keys are automatically omitted from the challenge header.

Accessing Payment Info

The middleware augments the Express request with payment information:
app.get('/api/user', (req, res) => {
  const { payerAddress, payload, version, verified } = req.payment;

  res.json({
    welcome: `Hello ${payerAddress}`,
    youPaid: payload.amount,
    token: payload.token
  });
});
Available Properties:
PropertyTypeDescription
payloadPaymentPayloadDecoded payment payload
payerAddressstringWallet address of the payer
version1 | 2Payment protocol version
verifiedbooleanWhether verification passed

Response Headers

On successful verification, the middleware adds:
X-Payment-Response: {"status":"verified","payerAddress":"0x...","version":2}

Error Responses

402 Payment Required

No payment header was provided.
{
  "error": "Payment required",
  "requirements": { ... }
}

400 Invalid Payload

Payment header exists but couldn’t be decoded.
{
  "error": "Invalid payment payload",
  "message": "..."
}

402 Verification Failed

Payment signature or amount was invalid.
{
  "error": "Verification failed: ..."
}

Tips

Express middleware runs in order — place paymentMiddleware before any routes that require payment.