Server middleware
npm install @tallypay/server express# ornpm install @tallypay/server honoTallyPayConfig
Section titled “TallyPayConfig”Both Express and Hono middleware accept the same configuration object:
| Property | Type | Required | Description |
|---|---|---|---|
facilitatorUrl | string | Yes | Base URL of your x402 facilitator (e.g. https://x402-facilitator.cdp.coinbase.com). The middleware calls POST {facilitatorUrl}/verify and POST {facilitatorUrl}/settle. |
payTo | string | Yes | Merchant receive address. Included in the 402 response body. |
price | string | Yes | Amount in base units (e.g. "10000" for 0.01 USDC with 6 decimals). |
network | string | Yes | CAIP-2 network identifier (e.g. "eip155:8453" for Base). |
asset | string | Yes | Asset name (e.g. "USDC"). |
apiKey | string | No | TallyPay API key (tp_live_…). Enables lifecycle event emission to the trace collector. Without it, payment gating still works but traces are off. |
collectorUrl | string | No | Override the collector URL (defaults to https://collector.tallypay.dev). Set to http://127.0.0.1:8787 for local dev. |
description | string | No | Human-readable description of what the payment unlocks. Included in the 402 body. |
Express
Section titled “Express”import express from "express";import { tallypay } from "@tallypay/server/express";
const app = express();
app.use( "/api/premium", tallypay({ facilitatorUrl: process.env.FACILITATOR_URL!, payTo: process.env.MERCHANT_ADDRESS!, price: "10000", network: "eip155:8453", asset: "USDC", apiKey: process.env.TALLYPAY_API_KEY, // optional description: "Premium data access", }),);
app.get("/api/premium", (_req, res) => { res.json({ message: "Paid content" });});
app.listen(3000);The middleware runs before your route handler. Unpaid requests never reach your handler — they receive a 402 with payment instructions.
import { Hono } from "hono";import { tallypay } from "@tallypay/server/hono";
const app = new Hono();
app.use( "/api/premium/*", tallypay({ facilitatorUrl: "https://x402-facilitator.cdp.coinbase.com", payTo: "0x...", price: "10000", network: "eip155:8453", asset: "USDC", }),);
app.get("/api/premium/data", (c) => c.json({ message: "Paid content" }));
export default app;Middleware flow
Section titled “Middleware flow”- Request arrives — generates a
traceId. - No
payment-signatureheader — returns402with:- JSON body:
{ x402Version: 1, accepts: [{ scheme, network, asset, payTo, maxAmountRequired, description }] } payment-requiredheader: base64-encoded version of the same bodyx-trace-idheader: the generated trace ID- Emits
402_ISSUED(ifapiKeyis set).
- JSON body:
payment-signatureheader present — forwards to facilitator:POST {facilitatorUrl}/verifywith the raw signature as the body.- Expects
{ valid: boolean, reason?: string }. - If invalid, returns
402+ error. EmitsPAYMENT_ERROR. POST {facilitatorUrl}/settlewith the same body.- Expects
{ settled: boolean, txHash?: string }. - If not settled, returns
402+ error. EmitsPAYMENT_ERROR.
- Settlement succeeds — sets
x-trace-idandpayment-responseheaders, callsnext()(your route handler runs). EmitsPAYMENT_COMPLETE. - Any exception — returns
500. EmitsPAYMENT_ERROR.
Lifecycle events emitted
Section titled “Lifecycle events emitted”| Event | When | Metadata |
|---|---|---|
402_ISSUED | No payment header | endpoint, method, price, network |
VERIFY_REQUESTED | Before calling /verify | endpoint |
VERIFY_RESULT | After /verify returns | valid, reason |
SETTLE_REQUESTED | Before calling /settle | endpoint |
SETTLE_RESULT | After /settle returns | settled, txHash |
PAYMENT_COMPLETE | Settlement succeeded | endpoint, txHash |
PAYMENT_ERROR | Any failure | step, reason or error |
All events are fire-and-forget. If the collector is unreachable, events are silently dropped. The middleware never adds latency to payment processing.
Facilitator HTTP contract
Section titled “Facilitator HTTP contract”The middleware expects your facilitator to expose:
POST /verify— body: raw payment signature string. Response:{ valid: boolean, reason?: string }.POST /settle— body: raw payment signature string. Response:{ settled: boolean, txHash?: string }.
Timeouts: 10s for verify, 30s for settle.
If your facilitator uses different paths or response shapes, write a thin HTTP proxy or adapter.
Without TallyPay tracing
Section titled “Without TallyPay tracing”Omit apiKey and the middleware is a pure open-source x402 gateway — no network calls to TallyPay, no telemetry. Payment gating works identically.
Next steps
Section titled “Next steps”- Wallet signing with viem — the client side of the flow
- React SDK — hooks and components
- Core primitives — types and event emitter