Replay Protection
Learn how x402 Pocket Nodes protects against replay attacks where attackers try to reuse payment proofs to gain unauthorized access.
What is a Replay Attack?
Section titled “What is a Replay Attack?”A replay attack occurs when an attacker:
- Intercepts a valid payment proof
- Captures the
X-Paymentheader - Attempts to reuse it for another request
- Tries to get free access using someone else’s payment
Without protection, this would allow:
- Unlimited API access with one payment
- Stolen payment proofs giving free access
- Compromised security of payment system
How x402 Prevents Replay Attacks
Section titled “How x402 Prevents Replay Attacks”1. Timestamp Validation
Section titled “1. Timestamp Validation”Every payment includes a timestamp:
{ "payload": { "timestamp": 1705318200 }}Server checks:
const now = Math.floor(Date.now() / 1000);const age = now - payment.timestamp;
if (age > 300) { // 5 minutes reject("Payment expired");}Protection:
- Old payments automatically rejected
- Attack window limited to 5 minutes
- Reduces replay attack viability
2. Signature Tracking
Section titled “2. Signature Tracking”Servers track used payment signatures:
// In-memory set (or database in production)const usedSignatures = new Set();
// On payment verificationif (usedSignatures.has(payment.signature)) { reject("Payment already used");}
// After successful verificationusedSignatures.add(payment.signature);Protection:
- Each signature can only be used once
- Even within the 5-minute window
- Duplicate attempts are rejected
3. Combined Protection
Section titled “3. Combined Protection”Both mechanisms work together:
Payment created at T=0 ↓Used at T=60 (1 minute) ✓ ↓Signature marked as used ↓Attempt to reuse at T=120 (2 minutes) ├─ Timestamp check: ✓ (< 5 min) └─ Signature check: ✗ (already used) ↓ Rejected!Implementation in Nodes
Section titled “Implementation in Nodes”Client Node
Section titled “Client Node”The x402 Client automatically:
- Generates fresh timestamp for each payment
- Creates unique signature for each payment
- Never reuses payment proofs
Every request gets a new payment:
Request 1 → Payment A (timestamp: T1, signature: S1)Request 2 → Payment B (timestamp: T2, signature: S2)Request 3 → Payment C (timestamp: T3, signature: S3)Mock Server Node
Section titled “Mock Server Node”The x402 Mock Server automatically:
- Checks payment timestamps
- Tracks used signatures
- Rejects duplicates
Storage: Node-level static data
const staticData = this.getWorkflowStaticData("node");const usedSignatures = staticData.usedSignatures || new Set();Attack Scenarios
Section titled “Attack Scenarios”Scenario 1: Immediate Replay
Section titled “Scenario 1: Immediate Replay”Attack:
1. Attacker captures X-Payment header2. Immediately reuses itDefense:
Server checks usedSignatures→ Signature already used→ Reject: "Payment already processed"Result: ✅ Blocked
Scenario 2: Delayed Replay
Section titled “Scenario 2: Delayed Replay”Attack:
1. Attacker captures X-Payment header at T=02. Waits 10 minutes3. Tries to use it at T=600Defense:
Server checks timestamp→ Age: 600 seconds (> 300 max)→ Reject: "Payment expired"Result: ✅ Blocked
Scenario 3: Modified Replay
Section titled “Scenario 3: Modified Replay”Attack:
1. Attacker captures X-Payment header2. Modifies amount or recipient3. Reuses with modificationsDefense:
Server verifies signature→ Message doesn't match signature→ Signature invalid for modified data→ Reject: "Invalid signature"Result: ✅ Blocked (signature wouldn’t verify)
Scenario 4: Cross-Endpoint Replay
Section titled “Scenario 4: Cross-Endpoint Replay”Attack:
1. Attacker pays for /api/cheap (0.01 USDC)2. Captures payment3. Tries to use for /api/expensive (1.00 USDC)Defense:
Server checks amount in signature→ Amount: 0.01 USDC→ Required: 1.00 USDC→ Reject: "Amount mismatch"Result: ✅ Blocked
Production Considerations
Section titled “Production Considerations”Signature Storage
Section titled “Signature Storage”In-Memory (Development):
const usedSignatures = new Set();Pros:
- Fast
- Simple
Cons:
- Lost on restart
- Doesn’t scale across instances
Redis (Production):
// Pseudocodeconst redis = new Redis();
async function isSignatureUsed(sig) { return await redis.exists(`sig:${sig}`);}
async function markSignatureUsed(sig) { // Expire after 5 minutes (payment timeout) await redis.setex(`sig:${sig}`, 300, "1");}Pros:
- Persists across restarts
- Shared across instances
- Automatic expiry
Cons:
- Requires Redis
- Network overhead
Database (Audit Trail):
await db.payments.insert({ signature: payment.signature, from: payment.from, amount: payment.amount, timestamp: payment.timestamp, resource: request.path, usedAt: new Date(),});Pros:
- Permanent audit trail
- Full payment history
- Analytics possible
Cons:
- Slower than Redis
- Storage grows over time
Cleanup Strategies
Section titled “Cleanup Strategies”Time-Based Cleanup:
// Remove signatures older than 5 minutessetInterval(() => { const now = Date.now() / 1000; for (const [sig, data] of processedPayments) { if (now - data.timestamp > 300) { processedPayments.delete(sig); } }}, 60000); // Every minuteSize-Based Cleanup:
// Keep only last N signaturesconst MAX_SIGNATURES = 10000;
if (usedSignatures.size > MAX_SIGNATURES) { // Remove oldest signatures (requires ordered storage) const sorted = [...usedSignatures].sort(); const toRemove = sorted.slice(0, 1000); toRemove.forEach((sig) => usedSignatures.delete(sig));}Multi-Instance Deployment
Section titled “Multi-Instance Deployment”Challenge
Section titled “Challenge”When running multiple server instances:
Instance A knows about payments it processedInstance B doesn't know about Instance A's payments→ Duplicate payment could work on Instance BSolution: Shared Storage
Section titled “Solution: Shared Storage”Use Redis or database for signature tracking:
Client → Load Balancer ├→ Instance A → Redis (check/store signatures) ├→ Instance B → Redis (check/store signatures) └→ Instance C → Redis (check/store signatures)All instances check the same signature store.
Mock Server Replay Protection
Section titled “Mock Server Replay Protection”How It Works
Section titled “How It Works”The x402 Mock Server uses node-level static data:
const staticData = this.getWorkflowStaticData("node");
// Track signaturesif (!staticData.usedSignatures) { staticData.usedSignatures = {};}
const signatureKey = `${signature}-${timestamp}`;
if (staticData.usedSignatures[signatureKey]) { reject("Payment already processed");}
staticData.usedSignatures[signatureKey] = { usedAt: new Date().toISOString(), from: payment.from, amount: payment.amount,};Persistence
Section titled “Persistence”Signatures persist across:
- ✅ Workflow executions
- ✅ n8n restarts
- ✅ Workflow edits
Lost on:
- ❌ Workflow deletion
- ❌ Manual static data clear
Testing Replay Attacks
Section titled “Testing Replay Attacks”Try to reuse a payment:
1. Make successful payment2. Copy X-Payment header value3. Make manual HTTP request with same header4. Should get: "Payment already processed"Best Practices
Section titled “Best Practices”1. Always Track Signatures
Section titled “1. Always Track Signatures”Even for testing:
- Builds good habits
- Reveals replay vulnerabilities
- Tests real-world scenarios
2. Use Appropriate TTL
Section titled “2. Use Appropriate TTL”5 minutes is good because:
- Enough time for network delays
- Short enough to limit replay window
- Standard in x402 protocol
3. Log Replay Attempts
Section titled “3. Log Replay Attempts”When duplicate detected:
console.warn("Replay attack detected:", { signature: payment.signature, originalTimestamp: storedData.timestamp, attemptTimestamp: payment.timestamp, from: payment.from,});
// Alert security team if threshold exceeded4. Monitor for Patterns
Section titled “4. Monitor for Patterns”Watch for:
- Multiple replay attempts from same wallet
- Systematic replay testing
- Coordinated attacks
5. Include in Tests
Section titled “5. Include in Tests”Test replay protection:
// First request - should succeedconst response1 = await client.makePayment();
// Second request with same payment - should failconst response2 = await client.reusePayment();expect(response2.error).toContain("already processed");Troubleshooting
Section titled “Troubleshooting”False Positives
Section titled “False Positives”Problem: Legitimate request rejected as duplicate
Causes:
- Client retry with same payment
- Network issue caused duplicate send
- Timestamp collision (very rare)
Solutions:
- Client creates new payment on retry
- Use signature + timestamp as key
- Log all rejections for analysis
Signature Storage Growing
Section titled “Signature Storage Growing”Problem: usedSignatures keeps growing
Solutions:
- Implement time-based cleanup
- Use Redis with TTL
- Store only recent signatures (last hour)
Cross-Instance Issues
Section titled “Cross-Instance Issues”Problem: Duplicate works on different instance
Solution:
- Use shared storage (Redis/Database)
- All instances check same signature store
- Synchronize via distributed cache
What’s Next?
Section titled “What’s Next?”- Custom Validation - Add custom checks
- Configuration - Advanced setup
- Mock Server - Test replay protection
- Security - Showcase server implementation