Skip to main content
The payment-identifier extension provides an idempotency mechanism for x402 payments. Clients can include a unique payment ID with their requests, and servers can use this ID to deduplicate payment processing - ensuring that retries with the same payment ID return cached responses without re-processing payments.

Use Cases

  • Network failures: Safely retry failed requests without duplicate payments
  • Client crashes: Resume requests after restart using persisted payment IDs
  • Load balancing: Same request can hit different servers with shared cache
  • Testing: Replay requests during development without spending funds

How It Works

  1. Server advertises payment-identifier extension support in the PaymentRequired response
  2. Client generates a unique payment ID and includes it in the PaymentPayload
  3. Server caches responses keyed by payment ID (with configurable TTL)
  4. Retry requests with the same payment ID return cached responses without re-processing payment

Quickstart for Buyers (Clients)

Step 1: Generate a Payment ID

Use the generatePaymentId() utility to create a unique identifier:
import { generatePaymentId } from "@x402/extensions/payment-identifier";

const paymentId = generatePaymentId();
// Example: "pay_7d5d747be160e280504c099d984bcfe0"

// Custom prefix
const orderId = generatePaymentId("order_");
// Example: "order_7d5d747be160e280504c099d984bcfe0"

Step 2: Add Payment ID to Extensions

Hook into the payment flow to add the payment ID before payload creation:
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import {
  appendPaymentIdentifierToExtensions,
  generatePaymentId,
} from "@x402/extensions/payment-identifier";

const client = new x402Client();
// ... register schemes ...

// Generate a unique payment ID for this logical request
const paymentId = generatePaymentId();

// Hook into payment flow to add the payment ID
client.onBeforePaymentCreation(async ({ paymentRequired }) => {
  if (paymentRequired.extensions) {
    // Only appends if server declared the extension
    appendPaymentIdentifierToExtensions(paymentRequired.extensions, paymentId);
  }
});

const fetchWithPayment = wrapFetchWithPayment(fetch, client);

// First request - payment is processed
const response1 = await fetchWithPayment(url);

// Retry with same payment ID - cached response returned (no payment)
const response2 = await fetchWithPayment(url);

Best Practices

  1. Generate payment IDs at the logical request level, not per retry
  2. Persist payment IDs for long-running operations so they survive restarts
  3. Use descriptive prefixes (e.g., generatePaymentId("order_")) to identify payment types
  4. Don’t reuse payment IDs across different logical requests

Quickstart for Sellers (Servers)

Step 1: Advertise Extension Support

Declare the payment-identifier extension in your route configuration:
import {
  paymentMiddlewareFromHTTPServer,
  x402ResourceServer,
  x402HTTPResourceServer,
} from "@x402/express";
import {
  declarePaymentIdentifierExtension,
  PAYMENT_IDENTIFIER,
} from "@x402/extensions/payment-identifier";

const routes = {
  "GET /weather": {
    accepts: [
      {
        scheme: "exact",
        price: "$0.001",
        network: "eip155:84532",
        payTo: address,
      },
    ],
    extensions: {
      [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(false), // optional
    },
  },
};
Optional vs Required:
// Payment ID is optional (clients can omit it)
declarePaymentIdentifierExtension(false)

// Payment ID is required (clients must provide it or receive 400 Bad Request)
declarePaymentIdentifierExtension(true)

Step 2: Cache Responses After Settlement

Store responses after successful payment settlement:
import { extractPaymentIdentifier } from "@x402/extensions/payment-identifier";

// In-memory cache (use Redis in production)
const idempotencyCache = new Map<string, { timestamp: number; response: unknown }>();
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour

const resourceServer = new x402ResourceServer(facilitatorClient)
  .register("eip155:84532", new ExactEvmScheme())
  .onAfterSettle(async ({ paymentPayload }) => {
    const paymentId = extractPaymentIdentifier(paymentPayload);
    if (paymentId) {
      idempotencyCache.set(paymentId, {
        timestamp: Date.now(),
        response: { /* your response data */ },
      });
    }
  });

Step 3: Check Cache Before Payment

Use the onProtectedRequest hook to return cached responses and skip payment processing:
const httpServer = new x402HTTPResourceServer(resourceServer, routes)
  .onProtectedRequest(async (context) => {
    if (!context.paymentHeader) return;

    try {
      const paymentPayload = JSON.parse(
        Buffer.from(context.paymentHeader, "base64").toString("utf-8"),
      );
      const paymentId = extractPaymentIdentifier(paymentPayload);

      if (paymentId) {
        const cached = idempotencyCache.get(paymentId);
        if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
          return { grantAccess: true }; // Skip payment, serve from cache
        }
      }
    } catch {
      // Invalid payment header, continue to normal payment flow
    }
  });

Idempotency Behavior

ScenarioServer Response
New payment IDProcess payment normally, cache response
Same payment ID (within TTL)Return cached response, skip payment
Same payment ID (after TTL)Process payment normally, update cache
No payment IDProcess payment normally (no caching)

Configuration Options

Cache TTL

Adjust CACHE_TTL_MS (TypeScript/Go) or CACHE_TTL_SECONDS (Python) based on your use case:
  • Short TTL (5-15 min): For time-sensitive resources
  • Long TTL (1-24 hours): For static or infrequently changing resources

Production Considerations

  1. Use Redis or similar instead of in-memory cache for distributed systems
  2. Handle cache failures gracefully - if cache is unavailable, process payment normally
  3. Consider payload hashing - for additional safety, hash the full payload and reject if same ID but different payload (409 Conflict)
  4. Monitor cache hit rates to tune TTL and detect abuse

API Reference

Client Functions

generatePaymentId(prefix?)

Generates a cryptographically secure unique payment identifier.
import { generatePaymentId } from "@x402/extensions/payment-identifier";

const paymentId = generatePaymentId();
// Returns: "pay_<32-character-hex-string>"

const orderId = generatePaymentId("order_");
// Returns: "order_<32-character-hex-string>"

appendPaymentIdentifierToExtensions(extensions, id?)

Adds a payment identifier to the extensions object. Only modifies extensions if the server declared support for the extension. If no payment ID is provided, one is generated automatically.
import { appendPaymentIdentifierToExtensions } from "@x402/extensions/payment-identifier";

const extensions = paymentRequired.extensions ?? {};
appendPaymentIdentifierToExtensions(extensions, "pay_custom_id_1234567890abcdef");
// extensions now contains the payment-identifier extension (only if server declared it)

isValidPaymentId(id)

Validates a payment identifier format.
import { isValidPaymentId } from "@x402/extensions/payment-identifier";

isValidPaymentId("pay_7d5d747be160e280504c099d984bcfe0"); // true
isValidPaymentId("invalid"); // false (too short)

Server Functions

declarePaymentIdentifierExtension(required?)

Creates a payment-identifier extension declaration for resource servers.
import { declarePaymentIdentifierExtension } from "@x402/extensions/payment-identifier";

// Optional payment ID (default)
const extension = declarePaymentIdentifierExtension();

// Required payment ID
const extensionRequired = declarePaymentIdentifierExtension(true);

extractPaymentIdentifier(paymentPayload)

Extracts the payment identifier from a payment payload.
import { extractPaymentIdentifier } from "@x402/extensions/payment-identifier";

const paymentId = extractPaymentIdentifier(paymentPayload);
if (paymentId) {
  // Check cache, implement idempotency logic
}

validatePaymentIdentifier(extension)

Validates the payment identifier extension object structure and ID format.
import { validatePaymentIdentifier } from "@x402/extensions/payment-identifier";

const extension = paymentPayload.extensions?.["payment-identifier"];
const result = validatePaymentIdentifier(extension);
if (!result.valid) {
  console.error(result.errors);
}

Constants

import {
  PAYMENT_IDENTIFIER,      // "payment-identifier"
  PAYMENT_ID_MIN_LENGTH,   // 16
  PAYMENT_ID_MAX_LENGTH,   // 128
  PAYMENT_ID_PATTERN,      // /^[a-zA-Z0-9_-]+$/
} from "@x402/extensions/payment-identifier";

Examples

Full working examples are available in the x402 repository: TypeScript: Python: Go:

FAQ

Q: What happens if I reuse a payment ID for a different request? A: The server will return the cached response from the first request. Don’t reuse payment IDs across different logical requests. Q: How long are payment IDs cached? A: This is configurable by the server. Typical TTLs range from 5 minutes to 24 hours depending on the use case. Q: Can I use custom payment ID formats? A: Payment IDs must be 16-128 characters, alphanumeric with hyphens and underscores allowed. Use isValidPaymentId() to validate custom IDs. Q: What if the server doesn’t support payment-identifier? A: The extension is optional. If the server doesn’t advertise support, clients can still make payments normally without idempotency.