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:
- Checks for payment headers (
X-Payment or X402-PAYMENT)
- Verifies the payment signature and amount
- Attaches payment info to
req.payment
- 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
| Network | Token | Address |
|---|
| Base | USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Ethereum | USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| Base | USDT | 0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb |
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:
| Property | Type | Description |
|---|
payload | PaymentPayload | Decoded payment payload |
payerAddress | string | Wallet address of the payer |
version | 1 | 2 | Payment protocol version |
verified | boolean | Whether verification passed |
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.