Production Deployment
This guide covers best practices for deploying T402 payment integration in production environments.
Deployment Checklist
Before going to production:
- Security audit completed
- Private keys secured in secret manager
- Monitoring and alerting configured
- Rate limiting enabled
- Error handling tested
- Backup and recovery procedures documented
- Load testing performed
- Configured production wallet addresses
- Tested payment flows on testnet
- Configured appropriate timeouts
- Configured backup facilitator (optional)
Infrastructure Architecture
┌─────────────────────────────────┐
│ Load Balancer │
│ (SSL Termination) │
└───────────────┬─────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
v v v
┌────────────┐ ┌────────────┐ ┌────────────┐
│ API-1 │ │ API-2 │ │ API-3 │
│ (t402) │ │ (t402) │ │ (t402) │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└─────────────────────┼─────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
v v v
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Redis │ │ PostgreSQL │ │ Facilitator│
│ (Cache) │ │ (Data) │ │ (t402.io) │
└────────────┘ └────────────┘ └────────────┘Environment Variables
# Required for all deployments
T402_FACILITATOR_URL=https://facilitator.t402.io
T402_PAY_TO=0xYourReceiverAddress
T402_NETWORK=eip155:8453
T402_SCHEME=exactSecure Secret Management
Never hardcode secrets. Use a secret manager:
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManager({ region: 'us-east-1' });
async function getSecrets() {
const response = await client.getSecretValue({
SecretId: 't402-production-secrets',
});
return JSON.parse(response.SecretString);
}
// Usage
const secrets = await getSecrets();
const facilitator = createFacilitatorClient({
url: 'https://facilitator.t402.io',
apiKey: secrets.T402_API_KEY,
});Configure Rate Limiting
Protect your API from abuse:
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
const limiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 't402:rate-limit:',
}),
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'Too many requests',
retryAfter: res.getHeader('Retry-After'),
});
},
});
app.use('/api/', limiter);Implement Health Checks
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.get('/ready', async (req, res) => {
try {
// Check Redis
await redis.ping();
// Check Facilitator
const supported = await facilitator.getSupported();
res.json({
status: 'ready',
checks: {
redis: 'ok',
facilitator: 'ok',
networks: supported.kinds.length,
},
});
} catch (error) {
res.status(503).json({
status: 'not ready',
error: error.message,
});
}
});Set Up Monitoring
import promClient from 'prom-client';
// Metrics
const paymentCounter = new promClient.Counter({
name: 't402_payments_total',
help: 'Total payment requests',
labelNames: ['network', 'status'],
});
const paymentDuration = new promClient.Histogram({
name: 't402_payment_duration_seconds',
help: 'Payment processing duration',
labelNames: ['network'],
buckets: [0.1, 0.5, 1, 2, 5, 10],
});
// Middleware to track payments
function trackPayments(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const network = req.paymentNetwork || 'unknown';
const status = res.statusCode < 400 ? 'success' : 'error';
paymentCounter.inc({ network, status });
paymentDuration.observe({ network }, duration);
});
next();
}
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.send(await promClient.register.metrics());
});Key Metrics
| Metric | Description | Alert Threshold |
|---|---|---|
t402_payments_total | Total payment attempts | - |
t402_payments_success | Successful payments | - |
t402_payments_failed | Failed payments | >5% failure rate |
t402_verification_latency | Time to verify payment | >5s p99 |
t402_settlement_latency | Time to settle payment | >30s p99 |
t402_facilitator_errors | Facilitator communication errors | >1% |
Configure Alerting
# alertmanager.yml
groups:
- name: t402
rules:
- alert: HighPaymentFailureRate
expr: rate(t402_payments_total{status="error"}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High payment failure rate"
description: "Payment failure rate is {{ $value }} per second"
- alert: FacilitatorUnreachable
expr: probe_success{job="facilitator"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Facilitator is unreachable"Implement Error Handling
import { PaymentError, NetworkError, SettlementError } from '@t402/core';
// Global error handler
app.use((err, req, res, next) => {
// Log error
logger.error('Request error', {
error: err.message,
stack: err.stack,
path: req.path,
method: req.method,
});
// Handle specific errors
if (err instanceof PaymentError) {
return res.status(402).json({
error: 'Payment failed',
code: err.code,
message: err.message,
});
}
if (err instanceof NetworkError) {
return res.status(503).json({
error: 'Network error',
message: 'Please try again later',
});
}
// Generic error
res.status(500).json({
error: 'Internal server error',
requestId: req.id,
});
});
// Retry with exponential backoff
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000,
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = baseDelay * Math.pow(2, i);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('Max retries exceeded');
}Database Schema
Store payment records:
CREATE TABLE payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
idempotency_key VARCHAR(64) UNIQUE,
network VARCHAR(50) NOT NULL,
amount VARCHAR(78) NOT NULL,
payer_address VARCHAR(100) NOT NULL,
recipient_address VARCHAR(100) NOT NULL,
transaction_hash VARCHAR(100),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
resource_path VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
settled_at TIMESTAMP,
metadata JSONB
);
CREATE INDEX idx_payments_network ON payments(network);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_created_at ON payments(created_at);Structured Logging
// Structured logging for payments
logger.info('Payment received', {
transactionId: result.transaction,
payer: result.payer,
amount: requirements.amount,
network: requirements.network,
duration: verificationTime,
});
logger.error('Payment failed', {
error: error.message,
errorCode: error.code,
payer: payload.payer,
network: requirements.network,
});Load Testing
Test your deployment under load:
// k6 load test script
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Ramp up
{ duration: '3m', target: 50 }, // Sustain
{ duration: '1m', target: 100 }, // Peak
{ duration: '1m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'], // <1% errors
},
};
export default function () {
const res = http.get('https://api.example.com/api/premium/data', {
headers: {
'Payment-Signature': VALID_PAYMENT_HEADER,
},
});
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}Docker Deployment
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Health check endpoint
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["npm", "start"]# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
environment:
- T402_FACILITATOR_URL=https://facilitator.t402.io
- T402_PAY_TO=${PAY_TO_ADDRESS}
- T402_NETWORK=eip155:8453
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: t402-api
spec:
replicas: 3
selector:
matchLabels:
app: t402-api
template:
metadata:
labels:
app: t402-api
spec:
containers:
- name: api
image: your-registry/t402-api:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: T402_FACILITATOR_URL
value: https://facilitator.t402.io
- name: T402_PAY_TO
valueFrom:
secretKeyRef:
name: t402-secrets
key: pay-to-address
- name: T402_NETWORK
value: "eip155:8453"
envFrom:
- secretRef:
name: t402-secrets
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: t402-api
spec:
selector:
app: t402-api
ports:
- port: 80
targetPort: 3000
type: ClusterIPSecurity Considerations
HTTPS
Always use HTTPS in production:
# nginx.conf
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Secure Key Management
⚠️
Never hardcode private keys in source code or environment variables in client-side code.
Server-side signing (recommended):
// Client requests signature from your backend
const response = await fetch('/api/sign-payment', {
method: 'POST',
body: JSON.stringify({ amount, recipient }),
});
const { signature } = await response.json();Hardware wallet integration:
import { createWalletClient, custom } from 'viem';
// Use browser wallet (MetaMask, etc.)
const walletClient = createWalletClient({
transport: custom(window.ethereum),
});Input Validation
// Validate payment headers
function validatePaymentHeader(header: string): boolean {
if (!header || header.length > 10000) {
return false;
}
try {
const decoded = Buffer.from(header, 'base64').toString('utf8');
const payload = JSON.parse(decoded);
// Validate required fields
if (!payload.t402Version || !payload.scheme || !payload.network) {
return false;
}
return true;
} catch {
return false;
}
}High Availability
Facilitator Failover
const facilitators = [
'https://facilitator.t402.io',
'https://backup-facilitator.example.com',
];
async function verifyWithFailover(payload, requirements) {
for (const url of facilitators) {
try {
const client = new HttpFacilitatorClient(url);
return await client.verify(payload, requirements);
} catch (error) {
console.warn(`Facilitator ${url} failed, trying next...`);
}
}
throw new Error('All facilitators unavailable');
}Circuit Breaker
import CircuitBreaker from 'opossum';
const breaker = new CircuitBreaker(verifyPayment, {
timeout: 10000, // 10 seconds
errorThresholdPercentage: 50,
resetTimeout: 30000, // 30 seconds
});
breaker.on('open', () => {
console.warn('Circuit breaker opened - facilitator may be down');
});Performance Optimization
Connection Pooling
// Reuse HTTP connections
import { Agent } from 'https';
const agent = new Agent({
keepAlive: true,
maxSockets: 100,
maxFreeSockets: 10,
});Caching
// Cache supported networks response
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 300 }); // 5 minutes
async function getSupportedNetworks() {
const cached = cache.get('supported');
if (cached) return cached;
const supported = await facilitator.getSupported();
cache.set('supported', supported);
return supported;
}Rollback Strategy
- Version your deployments - Use semantic versioning for releases
- Blue-green deployment - Keep previous version running during rollout
- Feature flags - Toggle payment features without redeployment
- Database migrations - Ensure backward compatibility
#!/bin/bash
# rollback.sh
# Get previous deployment
PREVIOUS=$(kubectl rollout history deployment/t402-api -o jsonpath='{.metadata.annotations.deployment\.kubernetes\.io/revision}')
PREVIOUS=$((PREVIOUS - 1))
# Rollback
kubectl rollout undo deployment/t402-api --to-revision=$PREVIOUS
# Wait and verify
kubectl rollout status deployment/t402-api
curl -f https://api.example.com/health || exit 1
echo "Rollback complete"Related
- Self-Hosted Facilitator - Run your own facilitator
- Best Practices - Development best practices
- Troubleshooting - Common issues