Production Deployment
This tutorial 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
Infrastructure Architecture
┌─────────────────────────────────┐
│ Load Balancer │
│ (SSL Termination) │
└───────────────┬─────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ API-1 │ │ API-2 │ │ API-3 │
│ (t402) │ │ (t402) │ │ (t402) │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└─────────────────────┼─────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Redis │ │ PostgreSQL │ │ Facilitator│
│ (Cache) │ │ (Data) │ │ (t402.io) │
└────────────┘ └────────────┘ └────────────┘Secure 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());
});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);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);
}Kubernetes 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
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: 10Rollback Procedures
Document and test rollback:
#!/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