Best Practices
Guidelines for building robust t402-enabled applications.
Payment Flow Design
Verify Before Processing
Always verify payments before executing expensive operations:
// Good: Verify first, then process
app.post('/api/compute', async (req, res) => {
const payment = extractPayment(req);
// Verify payment BEFORE doing expensive work
const verified = await facilitator.verify(payment, requirements);
if (!verified.isValid) {
return res.status(402).json({ error: 'Invalid payment' });
}
// Now do the expensive computation
const result = await expensiveComputation(req.body);
// Settle after success
await facilitator.settle(payment, requirements);
return res.json(result);
});Idempotent Settlements
Handle duplicate settlement attempts gracefully:
// Track processed payments
const processedPayments = new Set();
async function settlePayment(payment, requirements) {
const paymentId = `${payment.nonce}-${payment.from}`;
if (processedPayments.has(paymentId)) {
return { alreadyProcessed: true };
}
const result = await facilitator.settle(payment, requirements);
processedPayments.add(paymentId);
return result;
}Atomic Operations
Ensure payment and service delivery are atomic:
// Use database transactions
async function processPayment(payment, requirements) {
const tx = await db.beginTransaction();
try {
// Record payment intent
await tx.insert('payments', {
nonce: payment.nonce,
status: 'pending',
});
// Settle payment
const result = await facilitator.settle(payment, requirements);
// Update status
await tx.update('payments', {
status: 'completed',
txHash: result.transaction,
});
await tx.commit();
return result;
} catch (error) {
await tx.rollback();
throw error;
}
}Error Handling
Graceful Degradation
// Provide fallback for payment failures
async function handleRequest(req, res) {
try {
const payment = extractPayment(req);
const result = await processWithPayment(payment);
return res.json(result);
} catch (error) {
if (error.code === 'FACILITATOR_UNAVAILABLE') {
// Fallback to queued processing
await queueForLaterProcessing(req);
return res.status(202).json({
message: 'Payment queued for processing',
});
}
throw error;
}
}Informative Error Messages
// Return helpful error information
function handlePaymentError(error, res) {
const errorResponses = {
INVALID_SIGNATURE: {
status: 402,
message: 'Payment signature is invalid. Please sign again.',
},
EXPIRED_PAYMENT: {
status: 402,
message: 'Payment has expired. Create a new payment.',
},
INSUFFICIENT_FUNDS: {
status: 402,
message: 'Insufficient balance in payer wallet.',
},
NETWORK_MISMATCH: {
status: 400,
message: 'Payment network does not match required network.',
},
};
const response = errorResponses[error.code] || {
status: 500,
message: 'Payment processing failed',
};
return res.status(response.status).json({
error: response.message,
code: error.code,
retryable: error.retryable ?? false,
});
}Retry Logic
// Implement exponential backoff for transient failures
async function settleWithRetry(payment, requirements, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await facilitator.settle(payment, requirements);
} catch (error) {
if (!error.retryable || attempt === maxRetries) {
throw error;
}
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}Security
Validate All Inputs
import { z } from 'zod';
// Define strict schemas
const PaymentPayloadSchema = z.object({
t402Version: z.literal(2),
scheme: z.string().min(1),
network: z.string().regex(/^(eip155|solana|ton|tron|near|aptos|tezos|polkadot|stacks|cosmos):/),
payload: z.object({
authorization: z.object({
from: z.string(),
to: z.string(),
value: z.string(),
validAfter: z.string(),
validBefore: z.string(),
nonce: z.string(),
}),
signature: z.string(),
}),
});
function validatePayment(data: unknown) {
return PaymentPayloadSchema.parse(data);
}Protect Against Replay Attacks
⚠️
Each payment nonce should only be usable once. Track used nonces to prevent replay attacks.
// Use Redis for nonce tracking
import Redis from 'ioredis';
const redis = new Redis();
async function checkNonce(nonce: string, network: string) {
const key = `nonce:${network}:${nonce}`;
// Set with expiry (24 hours)
const set = await redis.set(key, '1', 'EX', 86400, 'NX');
if (!set) {
throw new Error('Payment nonce already used');
}
}Amount Validation
// Validate payment amounts match requirements
function validateAmount(payment, requirements) {
const paymentAmount = BigInt(payment.payload.authorization.value);
const requiredAmount = BigInt(requirements.maxAmountRequired);
if (paymentAmount < requiredAmount) {
throw new Error(
`Insufficient payment: got ${paymentAmount}, need ${requiredAmount}`
);
}
// Optional: Check for overpayment
const maxOverpayment = requiredAmount * 2n;
if (paymentAmount > maxOverpayment) {
throw new Error('Payment amount suspiciously high');
}
}Performance
Parallel Verification
// Verify multiple payments in parallel when possible
async function verifyBatch(payments) {
const results = await Promise.allSettled(
payments.map((p) => facilitator.verify(p.payload, p.requirements))
);
return results.map((r, i) => ({
payment: payments[i],
verified: r.status === 'fulfilled' && r.value.isValid,
error: r.status === 'rejected' ? r.reason : null,
}));
}Connection Reuse
// Create singleton facilitator client
let facilitatorClient: FacilitatorClient | null = null;
function getFacilitatorClient() {
if (!facilitatorClient) {
facilitatorClient = new HttpFacilitatorClient({
url: process.env.T402_FACILITATOR_URL,
timeout: 30000,
keepAlive: true,
});
}
return facilitatorClient;
}Efficient Route Matching
// Pre-compile route patterns
const routePatterns = new Map();
function compileRoutes(routes) {
for (const [pattern, config] of Object.entries(routes)) {
const [method, path] = pattern.split(' ');
const regex = pathToRegex(path);
routePatterns.set(pattern, { method, regex, config });
}
}
function matchRoute(method, path) {
for (const [pattern, { method: m, regex, config }] of routePatterns) {
if (m === method && regex.test(path)) {
return config;
}
}
return null;
}User Experience
Clear Payment Requirements
// Return detailed payment requirements
function buildPaymentRequired(route, req) {
return {
t402Version: 2,
accepts: route.accepts.map((option) => ({
...option,
// Include human-readable descriptions
description: route.description,
displayPrice: formatPrice(option.price),
networkName: getNetworkName(option.network),
})),
resource: {
path: req.path,
method: req.method,
description: route.description,
},
error: 'Payment required to access this resource',
};
}Progress Indicators
// For long-running settlements, provide status updates
app.get('/api/payment/:id/status', async (req, res) => {
const status = await getPaymentStatus(req.params.id);
res.json({
status: status.state, // 'pending', 'verifying', 'settling', 'completed'
progress: status.progress, // 0-100
message: status.message,
estimatedTime: status.estimatedTime,
});
});Helpful Error Pages
// Custom 402 response handler
function handle402(requirements, res) {
res.status(402).json({
...requirements,
// Add helpful context
help: {
documentation: 'https://docs.t402.io/getting-started',
supportedWallets: ['MetaMask', 'Coinbase Wallet', 'WalletConnect'],
faucets: {
'eip155:84532': 'https://faucet.base.org',
},
},
});
}Testing
Unit Tests
import { describe, it, expect, vi } from 'vitest';
describe('PaymentMiddleware', () => {
it('should return 402 when no payment provided', async () => {
const req = createMockRequest('/api/premium');
const res = createMockResponse();
await paymentMiddleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(402);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
t402Version: 2,
accepts: expect.any(Array),
})
);
});
it('should proceed when valid payment provided', async () => {
const req = createMockRequest('/api/premium', {
headers: { 'payment-signature': validPaymentHeader },
});
const res = createMockResponse();
vi.spyOn(facilitator, 'verify').mockResolvedValue({ isValid: true });
await paymentMiddleware(req, res, next);
expect(next).toHaveBeenCalled();
});
});Integration Tests
import { test, expect } from '@playwright/test';
test('complete payment flow', async ({ request }) => {
// Step 1: Request without payment
const initial = await request.get('/api/premium');
expect(initial.status()).toBe(402);
const requirements = await initial.json();
expect(requirements.accepts).toHaveLength(1);
// Step 2: Create payment
const payment = await createTestPayment(requirements.accepts[0]);
// Step 3: Request with payment
const paid = await request.get('/api/premium', {
headers: { 'payment-signature': encodePayment(payment) },
});
expect(paid.status()).toBe(200);
expect(paid.headers()['payment-response']).toBeDefined();
});Load Testing
// k6 load test script
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 50,
duration: '5m',
};
export default function () {
// Test 402 response time
const res = http.get('https://api.example.com/premium');
check(res, {
'status is 402': (r) => r.status === 402,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}Monitoring and Alerting
Key Metrics
// Track these metrics
const metrics = {
// Counters
paymentsTotal: new Counter('t402_payments_total'),
paymentsSuccess: new Counter('t402_payments_success'),
paymentsFailed: new Counter('t402_payments_failed'),
// Histograms
verificationLatency: new Histogram('t402_verification_latency'),
settlementLatency: new Histogram('t402_settlement_latency'),
// Gauges
pendingSettlements: new Gauge('t402_pending_settlements'),
};Alert Thresholds
| Metric | Warning | Critical |
|---|---|---|
| Payment failure rate | >5% | >10% |
| Verification latency p99 | >3s | >10s |
| Settlement latency p99 | >30s | >60s |
| Facilitator error rate | >1% | >5% |
| Pending settlements | >100 | >500 |
Documentation
API Documentation
/**
* Premium data endpoint
*
* @route GET /api/premium
* @group Premium - Premium data access
*
* @payment
* - scheme: exact
* - network: eip155:8453
* - price: $0.01
*
* @returns {object} 200 - Premium data
* @returns {PaymentRequired} 402 - Payment required
*/
app.get('/api/premium', handler);Changelog
Maintain a changelog for payment-related changes:
## [1.2.0] - 2026-01-15
### Added
- Support for TON payments on /api/premium endpoint
### Changed
- Increased price for /api/compute from $0.05 to $0.10
### Fixed
- Fixed race condition in settlement processingNext Steps
- Deployment Guide - Production deployment
- Gasless Payments - ERC-4337 configuration
- MCP Integration - AI agent setup