All errors follow a consistent format:
{
"error" : {
"code" : "error_code" ,
"message" : "Human-readable error message" ,
"details" : {
"additional" : "context"
}
}
}
HTTP Status Codes
Success - Request completed successfully
Bad Request - Invalid request or business logic error
Not Found - Resource doesn’t exist
Validation Error - Request data failed validation
Internal Error - Something went wrong on our end
Upstream Error - Error from upstream provider
Timeout - Request took too long
Error Codes
Client Errors (4xx)
Status : 400Request data is malformed or invalid. {
"error" : {
"code" : "invalid_request" ,
"message" : "Transaction hash must be a valid hex string" ,
"details" : {
"field" : "transaction_hash" ,
"provided" : "invalid_hash"
}
}
}
Common causes:
Missing required fields
Invalid address format
Malformed transaction hash
Invalid chain identifier
Status : 404Requested resource doesn’t exist. {
"error" : {
"code" : "not_found" ,
"message" : "Verification not found: vrf_invalid123"
}
}
Common causes:
Invalid verification_id
Invalid settlement_id
Resource was deleted
Status : 400Payment verification failed validation. {
"error" : {
"code" : "verification_failed" ,
"message" : "Invalid EIP-3009 signature" ,
"details" : {
"reason" : "Signature verification failed" ,
"recovered_address" : "0x..." ,
"expected_address" : "0x..."
}
}
}
Common causes:
Invalid signature
Wrong signer address
Amount mismatch
Token address mismatch
Authorization expired
Status : 400Merchant hasn’t approved USDC spending for fee collection. {
"error" : {
"code" : "insufficient_allowance" ,
"message" : "Merchant has not approved USDC spending for fee collection" ,
"details" : {
"required_approval" : "0.001" ,
"current_allowance" : "0" ,
"treasury_address" : "0xa92560dcaf2fb556dfee4d2599f021a511b94aae" ,
"merchant_address" : "0xa821f428ef8cc9f54a9915336a82220853059090"
}
}
}
Solution: # Approve USDC spending
python -m app.merchant_approval --network base-sepolia --amount 1000
Status : 400Settlement execution failed. {
"error" : {
"code" : "settlement_failed" ,
"message" : "FiatTokenV2: authorization is expired" ,
"details" : {
"reason" : "Authorization validBefore timestamp has passed" ,
"validBefore" : "1735689600" ,
"currentTime" : "1735780000"
}
}
}
Common causes:
Authorization expired
Authorization already used
Insufficient customer balance
Invalid authorization parameters
Status : 422Request data failed validation. {
"error" : {
"code" : "validation_error" ,
"message" : "Validation failed" ,
"details" : {
"field" : "expected_amount" ,
"error" : "Must be a positive integer string"
}
}
}
Server Errors (5xx)
Status : 500Internal server error. {
"error" : {
"code" : "internal_error" ,
"message" : "An internal error occurred"
}
}
Action: Retry the request after a short delay.
Status : 502Error from upstream provider. {
"error" : {
"code" : "upstream_error" ,
"message" : "API unavailable" ,
"details" : {
"upstream_status" : 503
}
}
}
Action: Retry with exponential backoff.
Status : 504Request timeout. {
"error" : {
"code" : "timeout" ,
"message" : "Request timeout after 30 seconds"
}
}
Action: Retry the request.
EIP-3009 Specific Errors
Authorization Expired
{
"error" : {
"code" : "settlement_failed" ,
"message" : "FiatTokenV2: authorization is expired" ,
"details" : {
"reason" : "Authorization validBefore timestamp has passed" ,
"validBefore" : "1735689600" ,
"currentTime" : "1735780000"
}
}
}
Solution: User must create new authorization with longer validity:
// ❌ Bad: 1 hour validity
validBefore : String ( Math . floor ( Date . now () / 1000 ) + 3600 );
// ✅ Good: 24 hour validity
validBefore : String ( Math . floor ( Date . now () / 1000 ) + 86400 );
Authorization Already Used
{
"error" : {
"code" : "settlement_failed" ,
"message" : "FiatTokenV2: authorization is used or canceled" ,
"details" : {
"reason" : "This nonce has already been used" ,
"nonce" : "0xee232e308a6647938a68aee1ae355b85a262e4e450b7fde55cd4e729e6444ce9"
}
}
}
Solution: Generate unique nonce for each payment:
// ✅ Generate unique nonce
const nonceBytes = new Uint8Array ( 32 );
window . crypto . getRandomValues ( nonceBytes );
const nonce =
"0x" +
Array . from ( nonceBytes )
. map (( b ) => b . toString ( 16 ). padStart ( 2 , "0" ))
. join ( "" );
Invalid Signature
{
"error" : {
"code" : "verification_failed" ,
"message" : "Invalid EIP-3009 signature" ,
"details" : {
"reason" : "Recovered signer does not match from address" ,
"recovered" : "0x1234..." ,
"expected" : "0x5678..."
}
}
}
Solution: Ensure signature is created correctly:
// Use eth_signTypedData_v4 for EIP-712
const signature = await window . ethereum . request ({
method: "eth_signTypedData_v4" ,
params: [ userAddress , JSON . stringify ( typedData )],
});
Error Handling Flow
Error Handling Best Practices
async function handlePayment ( authorization , signature ) {
try {
// 1. Verify payment
const verifyResponse = await fetch (
"https://facilitator.0xmeta.ai/v1/verify" ,
{
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
transaction_hash: authorization . nonce ,
chain: "base-sepolia" ,
seller_address: merchantAddress ,
expected_amount: authorization . value ,
expected_token: authorization . token ,
metadata: {
source: "my_app" ,
paymentPayload: {
x402Version: 1 ,
scheme: "exact" ,
network: "base-sepolia" ,
payload: { authorization , signature },
},
},
}),
}
);
if ( ! verifyResponse . ok ) {
const error = await verifyResponse . json ();
handleVerificationError ( error );
return ;
}
const { verification_id } = await verifyResponse . json ();
// 2. Settle payment
const settleResponse = await fetch (
"https://facilitator.0xmeta.ai/v1/settle" ,
{
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
verification_id ,
destination_address: merchantAddress ,
}),
}
);
if ( ! settleResponse . ok ) {
const error = await settleResponse . json ();
handleSettlementError ( error );
return ;
}
const settlement = await settleResponse . json ();
console . log ( "Settlement initiated:" , settlement . settlement_id );
} catch ( error ) {
console . error ( "Network error:" , error );
// Retry logic here
}
}
function handleVerificationError ( error ) {
switch ( error . error . code ) {
case "verification_failed" :
if ( error . error . details ?. reason ?. includes ( "expired" )) {
alert ( "Authorization expired. Please try again." );
} else if ( error . error . details ?. reason ?. includes ( "signature" )) {
alert ( "Invalid signature. Please reconnect your wallet." );
}
break ;
case "invalid_request" :
console . error ( "Invalid request:" , error . error . details );
break ;
default :
alert ( "Verification failed: " + error . error . message );
}
}
function handleSettlementError ( error ) {
switch ( error . error . code ) {
case "insufficient_allowance" :
alert (
"Merchant setup required. Please contact support. \n\n " +
"Treasury: " +
error . error . details . treasury_address
);
break ;
case "settlement_failed" :
if ( error . error . message . includes ( "expired" )) {
alert ( "Payment authorization expired. Please try again." );
} else if ( error . error . message . includes ( "used or canceled" )) {
alert ( "This payment has already been processed." );
}
break ;
case "not_found" :
alert ( "Verification not found. Please start over." );
break ;
default :
alert ( "Settlement failed: " + error . error . message );
}
}
Retry Strategy
For transient errors (5xx, network errors), implement exponential backoff:
async function retryRequest ( fn , maxRetries = 3 ) {
for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
try {
return await fn ();
} catch ( error ) {
const shouldRetry =
error . status >= 500 ||
error . code === "upstream_error" ||
error . code === "timeout" ||
error . code === "ETIMEDOUT" ;
if ( ! shouldRetry || attempt === maxRetries - 1 ) {
throw error ;
}
// Exponential backoff: 1s, 2s, 4s
const delay = Math . pow ( 2 , attempt ) * 1000 ;
console . log ( `Retry attempt ${ attempt + 1 } after ${ delay } ms` );
await new Promise (( resolve ) => setTimeout ( resolve , delay ));
}
}
}
// Usage
try {
const result = await retryRequest (() => verifyPayment ( data ));
} catch ( error ) {
console . error ( "All retry attempts failed:" , error );
}
Don’t retry on 4xx errors (except possibly insufficient_allowance after
fixing). These indicate problems with your request that won’t be fixed by
retrying.
Error Prevention Checklist
Before Verification
✅ Generate unique random nonce
✅ Set validBefore to 24+ hours
✅ Use correct token address for network
✅ Format amounts correctly (no decimals in amount string)
✅ Include complete paymentPayload in metadata
Before Settlement
✅ Merchant has approved USDC spending - ✅ Verification status is
“verified” - ✅ Authorization hasn’t expired - ✅ Nonce hasn’t been used
before
Error Handling
✅ Catch and handle all error codes
✅ Show user-friendly error messages
✅ Log errors for debugging
✅ Implement retry logic for 5xx errors
✅ Don’t retry 4xx errors automatically
Common Error Scenarios
Scenario Error Code Solution Merchant not approved insufficient_allowanceRun approval setup Authorization expired settlement_failedCreate new authorization Nonce reused settlement_failedGenerate new unique nonce Invalid signature verification_failedFix signature creation Wrong amount verification_failedMatch expected_amount exactly Network issues upstream_error / timeoutRetry with backoff
Most errors are preventable! Follow best practices for nonce generation,
authorization validity, and merchant setup.
Log all errors with full context (verification_id, settlement_id, timestamps)
to help with debugging and support requests.