Skip to content

Nonce Management

The nonce system is a critical security feature that prevents replay attacks and ensures request uniqueness in the Synthetix APIs. Every authenticated request must include an incrementing nonce.

Overview

A nonce (number used once) serves two purposes:

  1. Replay Protection: Prevents malicious actors from re-submitting your signed requests
  2. Request Ordering: Ensures requests are processed in the intended order

Nonce Requirements

Format

  • Must be a positive integer
  • Recommended: Use an incrementing sequence (for example Date.now() with conflict handling)
  • Must be strictly increasing (each nonce > previous nonce)
  • Must be unique (must not be reused for the same signer/subaccount context)
  • Maximum value: 9223372036854775807 (max safe integer)

Example

{
  "action": {
    "action": "placeOrders",
    "order": { ... }
  },
  "nonce": 1704067200000,  // Positive integer nonce
  "signature": { ... }
}

Implementation Notes

1. Use Timestamp as a Convenience Nonce Source

One convenient approach is using the current timestamp:

function generateNonce() {
  return Date.now();  // Returns a positive integer nonce value
}
 
// Example: 1704067200000
import time
 
def generate_nonce():
    return int(time.time() * 1000)  # Returns a positive integer nonce value
 
# Example: 1704067200000

2. Handle Nonce Conflicts

If multiple requests are sent with the same base value, increment the nonce:

class NonceManager {
  constructor() {
    this.lastNonce = 0;
  }
 
  generateNonce() {
    const timestamp = Date.now();
    // Ensure nonce is always increasing
    this.lastNonce = Math.max(timestamp, this.lastNonce + 1);
    return this.lastNonce;
  }
}
 
const nonceManager = new NonceManager();
const nonce1 = nonceManager.generateNonce(); // 1704067200000
const nonce2 = nonceManager.generateNonce(); // 1704067200001 (incremented)

3. Synchronized Nonce Across Systems

When running multiple trading systems with the same account:

class DistributedNonceManager {
  constructor(systemId, totalSystems) {
    this.systemId = systemId;      // 0, 1, 2, etc.
    this.totalSystems = totalSystems;
    this.counter = 0;
  }
 
  generateNonce() {
    // Each system uses a different base value
    const baseTime = Date.now();
    const offset = this.systemId;
 
    // Ensure uniqueness across systems
    return baseTime * this.totalSystems + offset;
  }
}
 
// System 1: nonces end in 0: 17040672000000, 17040672000010, ...
// System 2: nonces end in 1: 17040672000001, 17040672000011, ...

Error Handling

Nonce Already Used Error

When a nonce has already been used:

{
  "status": "error",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Nonce already used",
    "details": {
      "lastNonce": 1704067200000,
      "attemptedNonce": 1704067199000
    }
  },
  "request_id": "5ccf215d37e3ae6d",
  "timestamp": "2025-01-01T00:00:00Z"
}

Recovery Strategy

async function executeWithNonceRetry(action, maxRetries = 3) {
  const nonceManager = new NonceManager();
 
  for (let i = 0; i < maxRetries; i++) {
    try {
      const nonce = nonceManager.generateNonce();
      const response = await sendRequest({
        action,
        nonce,
        signature: signRequest(action, nonce)
      });
      return response;
    } catch (error) {
      if (error.message.includes('Nonce already used') && i < maxRetries - 1) {
        // Force increment for next attempt
        nonceManager.lastNonce += 1000;
        continue;
      }
      throw error;
    }
  }
}

Advanced Nonce Strategies

1. High-Frequency Trading

For HFT systems sending hundreds of requests per second:

class HFTNonceManager {
  constructor() {
    this.baseTime = Date.now();
    this.counter = 0;
  }
 
  generateNonce() {
    // Combine timestamp with microsecond counter
    const elapsed = Date.now() - this.baseTime;
    this.counter++;
 
    // Format: [timestamp][counter]
    // Ensures uniqueness even within same millisecond
    return parseInt(`${Date.now()}${String(this.counter).padStart(6, '0')}`);
  }
 
  reset() {
    // Reset counter periodically to prevent overflow
    if (this.counter > 999999) {
      this.baseTime = Date.now();
      this.counter = 0;
    }
  }
}

2. Batch Operations

When sending batch operations, use sequential nonces:

function generateBatchNonces(batchSize) {
  const baseNonce = Date.now();
  const nonces = [];
 
  for (let i = 0; i < batchSize; i++) {
    nonces.push(baseNonce + i);
  }
 
  return nonces;
}
 
// Example: Placing 3 orders in parallel
const nonces = generateBatchNonces(3);
// [1704067200000, 1704067200001, 1704067200002]

3. Fallback Nonce Sources

Multiple nonce generation strategies for reliability:

class RobustNonceManager {
  constructor() {
    this.lastNonce = 0;
  }
 
  generateNonce() {
    const strategies = [
      () => Date.now(),
      () => Date.now() * 1000 + Math.floor(Math.random() * 1000),
      () => this.lastNonce + 1000,
      () => BigInt(Date.now()) * 1000000n + BigInt(process.hrtime()[1])
    ];
 
    for (const strategy of strategies) {
      try {
        const nonce = Number(strategy());
        if (nonce > this.lastNonce) {
          this.lastNonce = nonce;
          return nonce;
        }
      } catch (e) {
        continue;
      }
    }
 
    // Ultimate fallback
    this.lastNonce += 1;
    return this.lastNonce;
  }
}

Monitoring and Debugging

Nonce Tracking

Keep track of nonces for debugging:

class DebugNonceManager {
  constructor() {
    this.lastNonce = 0;
    this.nonceHistory = [];
  }
 
  generateNonce() {
    const nonce = Date.now();
    this.lastNonce = Math.max(nonce, this.lastNonce + 1);
 
    // Track for debugging
    this.nonceHistory.push({
      nonce: this.lastNonce,
      timestamp: new Date().toISOString(),
      stack: new Error().stack
    });
 
    // Keep only last 100 for memory efficiency
    if (this.nonceHistory.length > 100) {
      this.nonceHistory.shift();
    }
 
    return this.lastNonce;
  }
 
  getHistory() {
    return this.nonceHistory;
  }
}

Common Issues and Solutions

IssueCauseSolution
"Nonce already used"System clock driftSync system time with NTP
"Nonce must be greater"Nonce decreasedEnsure nonce is always increasing
Frequent conflictsMultiple systemsUse distributed nonce strategy
Integer overflowNonce too largeReset or use modulo arithmetic

Security

  • Never Reuse Nonces: Each nonce must be unique and increasing per account
  • Don't Use Predictable Patterns: While timestamps are recommended, add randomness for sensitive operations
  • Validate Locally: Check nonce increment before sending to avoid unnecessary API calls
  • Secure Storage: If persisting last nonce, store securely to prevent tampering

Implementation Examples

Full TypeScript Example

interface NonceManager {
  generateNonce(): bigint;
}
 
class ProductionNonceManager implements NonceManager {
  private lastNonce: bigint = 0n;
  private readonly maxNonce = BigInt(Number.MAX_SAFE_INTEGER);
 
  generateNonce(): bigint {
    const timestamp = BigInt(Date.now());
 
    // Ensure always increasing
    this.lastNonce = timestamp > this.lastNonce ? timestamp : this.lastNonce + 1n;
 
    // Check bounds
    if (this.lastNonce > this.maxNonce) {
      throw new Error('Nonce overflow - reset required');
    }
 
    return this.lastNonce;
  }
 
  reset(): void {
    this.lastNonce = 0n;
  }
}

Python Example

import time
import threading
from typing import Optional
 
class NonceManager:
    def __init__(self):
        self.last_nonce: int = 0
        self.lock = threading.Lock()
 
    def generate_nonce(self) -> int:
        with self.lock:
            timestamp = int(time.time() * 1000)
            self.last_nonce = max(timestamp, self.last_nonce + 1)
            return self.last_nonce
 
    def reset(self) -> None:
        with self.lock:
            self.last_nonce = 0
 
# Global instance for thread safety
nonce_manager = NonceManager()

Testing Nonce Management

Always test your nonce management system:

// Test uniqueness
const manager = new NonceManager();
const nonces = Array(1000).fill(0).map(() => manager.generateNonce());
const uniqueNonces = new Set(nonces);
console.assert(uniqueNonces.size === nonces.length, 'All nonces must be unique');
 
// Test incrementing
const isIncrementing = nonces.every((n, i) => i === 0 || n > nonces[i - 1]);
console.assert(isIncrementing, 'Nonces must be incrementing');

Integration with EIP-712 Signing

When using nonces with EIP-712 signatures:

import { SynthetixSigner } from './eip-712-signing';
 
class AuthenticatedRequestManager {
  constructor(privateKey) {
    this.signer = new SynthetixSigner(privateKey);
    this.nonceManager = new NonceManager();
  }
 
  async placeOrders(orders, expiresAfter) {
    const nonce = this.nonceManager.generateNonce();
    const signature = await this.signer.signPlaceOrders(orders, nonce, expiresAfter);
 
    return {
      action: {
        type: "placeOrders",
        orders
      },
      nonce,
      signature,
      expiresAfter
    };
  }
}

Next Steps

Related Documentation