GuidesProduction Deployment

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=exact

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());
});

Key Metrics

MetricDescriptionAlert Threshold
t402_payments_totalTotal payment attempts-
t402_payments_successSuccessful payments-
t402_payments_failedFailed payments>5% failure rate
t402_verification_latencyTime to verify payment>5s p99
t402_settlement_latencyTime to settle payment>30s p99
t402_facilitator_errorsFacilitator 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: 3

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
            - 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: ClusterIP

Security 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

  1. Version your deployments - Use semantic versioning for releases
  2. Blue-green deployment - Keep previous version running during rollout
  3. Feature flags - Toggle payment features without redeployment
  4. 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"