Documentation Index
Fetch the complete documentation index at: https://coinbase-5dac824f-mintlify-docs-update-1777927447315.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
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
- Server advertises
payment-identifier extension support in the PaymentRequired response
- Client generates a unique payment ID and includes it in the
PaymentPayload
- Server caches responses keyed by payment ID (with configurable TTL)
- 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);
Step 1: Generate a Payment ID
Use the generate_payment_id() utility to create a unique identifier:from x402.extensions.payment_identifier import generate_payment_id
payment_id = generate_payment_id()
# Example: "pay_7d5d747be160e280504c099d984bcfe0"
# Custom prefix
order_id = generate_payment_id("order_")
# Example: "order_7d5d747be160e280504c099d984bcfe0"
Step 2: Add Payment ID to Extensions
Hook into the payment flow to add the payment ID before payload creation:from x402 import x402Client
from x402.extensions.payment_identifier import (
append_payment_identifier_to_extensions,
generate_payment_id,
)
from x402.http.clients import x402HttpxClient
from x402.schemas import PaymentCreationContext
client = x402Client()
# ... register schemes ...
# Generate a unique payment ID for this logical request
payment_id = generate_payment_id()
# Hook into payment flow to add the payment ID
async def before_payment_creation(context: PaymentCreationContext) -> None:
extensions = context.payment_required.extensions
if extensions is not None:
# Only appends if server declared the extension
append_payment_identifier_to_extensions(extensions, payment_id)
client.on_before_payment_creation(before_payment_creation)
async with x402HttpxClient(client) as http:
# First request - payment is processed
response1 = await http.get(url)
# Retry with same payment ID - cached response returned (no payment)
response2 = await http.get(url)
Step 1: Generate a Payment ID
Use the GeneratePaymentID() utility to create a unique identifier:import "github.com/x402-foundation/x402/go/extensions/paymentidentifier"
// Generate with default prefix "pay_"
paymentID := paymentidentifier.GeneratePaymentID("")
// Example: "pay_7d5d747be160e280504c099d984bcfe0"
// Generate with custom prefix
paymentID = paymentidentifier.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 (
x402 "github.com/x402-foundation/x402/go"
"github.com/x402-foundation/x402/go/extensions/paymentidentifier"
)
client := x402.Newx402Client()
// ... register schemes ...
// Generate a unique payment ID for this logical request
paymentID := paymentidentifier.GeneratePaymentID("")
// Hook into payment flow to add the payment ID
client.OnBeforePaymentCreation(func(ctx x402.PaymentCreationContext) (*x402.BeforePaymentCreationHookResult, error) {
if ctx.Extensions == nil {
return nil, nil
}
// Only add if server declared the extension
if ctx.Extensions[paymentidentifier.PAYMENT_IDENTIFIER] == nil {
return nil, nil
}
err := paymentidentifier.AppendPaymentIdentifierToExtensions(ctx.Extensions, paymentID)
if err != nil {
return nil, err
}
return nil, nil
})
// First request - payment is processed
response1, err := client.MakeRequest(url)
// Retry with same payment ID - cached response returned (no payment)
response2, err := client.MakeRequest(url)
Best Practices
- Generate payment IDs at the logical request level, not per retry
- Persist payment IDs for long-running operations so they survive restarts
- Use descriptive prefixes (e.g.,
generatePaymentId("order_")) to identify payment types
- 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
}
});
Step 1: Advertise Extension Support
Declare the payment-identifier extension in your route configuration:from x402.server import x402ResourceServer
from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption
from x402.http.middleware.fastapi import PaymentMiddlewareASGI
from x402.http.types import RouteConfig
from x402.mechanisms.evm.exact import ExactEvmServerScheme
from x402.extensions.payment_identifier import (
declare_payment_identifier_extension,
PAYMENT_IDENTIFIER,
)
routes = {
"GET /weather": RouteConfig(
accepts=[
PaymentOption(
scheme="exact",
price="$0.001",
network="eip155:84532",
pay_to=address,
),
],
extensions={
PAYMENT_IDENTIFIER: declare_payment_identifier_extension(required=False), # optional
},
),
}
Optional vs Required:# Payment ID is optional (clients can omit it)
declare_payment_identifier_extension(required=False)
# Payment ID is required (clients must provide it or receive 400 Bad Request)
declare_payment_identifier_extension(required=True)
Step 2: Cache Responses After Settlement
Store responses after successful payment settlement:import time
from x402.schemas import SettleContext
from x402.extensions.payment_identifier import extract_payment_identifier
# In-memory cache (use Redis in production)
idempotency_cache: dict = {}
CACHE_TTL_SECONDS = 60 * 60 # 1 hour
async def after_settle(ctx: SettleContext) -> None:
payment_id = extract_payment_identifier(ctx.payment_payload)
if payment_id:
idempotency_cache[payment_id] = {
"timestamp": time.time(),
"response": {}, # your response data
}
server = x402ResourceServer(facilitator)
server.register("eip155:84532", ExactEvmServerScheme())
server.on_after_settle(after_settle)
Step 3: Check Cache Before Payment
Use FastAPI middleware to check the cache before the payment middleware processes the request:import base64
import json
from fastapi import Request, Response
from x402.schemas import PaymentPayload
@app.middleware("http")
async def idempotency_middleware(request: Request, call_next):
payment_header = request.headers.get("X-Payment")
if payment_header:
try:
payment_data = json.loads(base64.b64decode(payment_header))
payment_payload = PaymentPayload.model_validate(payment_data)
payment_id = extract_payment_identifier(payment_payload)
if payment_id:
cached = idempotency_cache.get(payment_id)
if cached and time.time() - cached["timestamp"] < CACHE_TTL_SECONDS:
return Response(
content=json.dumps(cached["response"]),
media_type="application/json",
)
except Exception:
pass # Invalid payment header, continue to normal flow
return await call_next(request)
# Add payment middleware AFTER idempotency middleware
app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server)
Step 1: Advertise Extension Support
Declare the payment-identifier extension in your route configuration:import (
x402http "github.com/x402-foundation/x402/go/http"
"github.com/x402-foundation/x402/go/extensions/paymentidentifier"
)
// Optional or required payment identifier (pick one)
paymentIdExtension := paymentidentifier.DeclarePaymentIdentifierExtension(false) // optional
// paymentIdExtension = paymentidentifier.DeclarePaymentIdentifierExtension(true) // required
routes := x402http.RoutesConfig{
"GET /weather": {
Accepts: []x402http.PaymentOption{
{
Scheme: "exact",
Price: "$0.001",
Network: "eip155:84532",
PayTo: address,
},
},
Extensions: map[string]interface{}{
paymentidentifier.PAYMENT_IDENTIFIER: paymentIdExtension,
},
},
}
Use the ExtractPaymentIdentifier() utility to get the payment ID from the payload:import "github.com/x402-foundation/x402/go/extensions/paymentidentifier"
// In your handler
payload := c.MustGet("x402_payload").(x402.PaymentPayload)
paymentID, err := paymentidentifier.ExtractPaymentIdentifier(payload, true)
if err != nil {
// Handle invalid payment ID
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Check for duplicate
if existingResponse, found := processedPayments[paymentID]; found {
// Return cached response
c.JSON(200, existingResponse)
return
}
Step 3: Cache Responses
Store responses after successful payment processing:// In-memory cache (use Redis in production)
var processedPayments = make(map[string]interface{})
// After processing payment
processedPayments[paymentID] = responseData
Idempotency Behavior
| Scenario | Server Response |
|---|
| New payment ID | Process 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 ID | Process 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
- Use Redis or similar instead of in-memory cache for distributed systems
- Handle cache failures gracefully - if cache is unavailable, process payment normally
- Consider payload hashing - for additional safety, hash the full payload and reject if same ID but different payload (409 Conflict)
- 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);
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";
Client Functions
generate_payment_id(prefix="pay_")
Generates a cryptographically secure unique payment identifier.from x402.extensions.payment_identifier import generate_payment_id
payment_id = generate_payment_id()
# Returns: "pay_<32-character-hex-string>"
order_id = generate_payment_id("order_")
# Returns: "order_<32-character-hex-string>"
append_payment_identifier_to_extensions(extensions, id=None)
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.from x402.extensions.payment_identifier import append_payment_identifier_to_extensions
extensions = payment_required.extensions or {}
append_payment_identifier_to_extensions(extensions, "pay_custom_id_1234567890abcdef")
# extensions now contains the payment-identifier extension (only if server declared it)
is_valid_payment_id(id)
Validates a payment identifier format.from x402.extensions.payment_identifier import is_valid_payment_id
is_valid_payment_id("pay_7d5d747be160e280504c099d984bcfe0") # True
is_valid_payment_id("invalid") # False (too short)
Server Functions
declare_payment_identifier_extension(required=False)
Creates a payment-identifier extension declaration for resource servers.from x402.extensions.payment_identifier import declare_payment_identifier_extension
# Optional payment ID (default)
extension = declare_payment_identifier_extension()
# Required payment ID
extension_required = declare_payment_identifier_extension(required=True)
Extracts the payment identifier from a payment payload.from x402.extensions.payment_identifier import extract_payment_identifier
payment_id = extract_payment_identifier(payment_payload)
if payment_id:
# Check cache, implement idempotency logic
pass
validate_payment_identifier(extension)
Validates the payment identifier extension object structure and ID format.from x402.extensions.payment_identifier import validate_payment_identifier
extension = payment_payload.extensions.get("payment-identifier")
result = validate_payment_identifier(extension)
if not result.valid:
print(result.errors)
Constants
from x402.extensions.payment_identifier import (
PAYMENT_IDENTIFIER, # "payment-identifier"
PAYMENT_ID_MIN_LENGTH, # 16
PAYMENT_ID_MAX_LENGTH, # 128
PAYMENT_ID_PATTERN, # re.compile(r"^[a-zA-Z0-9_-]+$")
)
Client Functions
GeneratePaymentID(prefix string)
Generates a cryptographically secure unique payment identifier.import "github.com/x402-foundation/x402/go/extensions/paymentidentifier"
// Generate with default prefix "pay_"
paymentID := paymentidentifier.GeneratePaymentID("")
// Returns: "pay_<32-character-hex-string>"
// Generate with custom prefix
paymentID = paymentidentifier.GeneratePaymentID("order_")
// Returns: "order_<32-character-hex-string>"
AppendPaymentIdentifierToExtensions(extensions map[string]interface{}, id string) error
Adds a payment identifier to the extensions object. Only modifies extensions if the server declared support for the extension. Pass an empty string to auto-generate an ID.import "github.com/x402-foundation/x402/go/extensions/paymentidentifier"
extensions := make(map[string]interface{})
err := paymentidentifier.AppendPaymentIdentifierToExtensions(extensions, "pay_custom_id_1234567890abcdef")
// extensions now contains the payment-identifier extension (only if server declared it)
IsValidPaymentID(id string) bool
Validates a payment identifier format.import "github.com/x402-foundation/x402/go/extensions/paymentidentifier"
valid := paymentidentifier.IsValidPaymentID("pay_7d5d747be160e280504c099d984bcfe0") // true
valid = paymentidentifier.IsValidPaymentID("invalid") // false (too short)
Server Functions
DeclarePaymentIdentifierExtension(required bool) PaymentIdentifierExtension
Creates a payment-identifier extension declaration for resource servers.import "github.com/x402-foundation/x402/go/extensions/paymentidentifier"
// Optional payment ID
extension := paymentidentifier.DeclarePaymentIdentifierExtension(false)
// Required payment ID
extensionRequired := paymentidentifier.DeclarePaymentIdentifierExtension(true)
Extracts the payment identifier from a payment payload.import "github.com/x402-foundation/x402/go/extensions/paymentidentifier"
paymentID, err := paymentidentifier.ExtractPaymentIdentifier(payload, true)
if err != nil {
// Handle error
}
if paymentID != "" {
// Check cache, implement idempotency logic
}
ValidatePaymentIdentifier(extension interface{}) ValidationResult
Validates the payment identifier extension object structure and ID format.import "github.com/x402-foundation/x402/go/extensions/paymentidentifier"
extension := payload.Extensions[paymentidentifier.PAYMENT_IDENTIFIER]
result := paymentidentifier.ValidatePaymentIdentifier(extension)
if !result.Valid {
// Handle validation errors
fmt.Println(result.Errors)
}
Constants
import "github.com/x402-foundation/x402/go/extensions/paymentidentifier"
const (
PAYMENT_IDENTIFIER = "payment-identifier"
PAYMENT_ID_MIN_LENGTH = 16
PAYMENT_ID_MAX_LENGTH = 128
)
var PAYMENT_ID_PATTERN = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
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.