Skip to content

Rail Failures

Diagnosing rail failures

All rail errors surface as PQSafeError with a rail field indicating which adapter failed:

import { PQSafeError } from '@pqsafe/agent-pay'
try {
await executeAgentPayment(signed, request)
} catch (err) {
if (err instanceof PQSafeError) {
console.log('Code:', err.code)
console.log('Rail:', err.rail) // 'airwallex' | 'wise' | 'stripe' | 'usdc-base' | 'x402'
console.log('Details:', err.details) // Raw rail API error
}
}

Rail-specific failure modes

Airwallex

ErrorCauseFix
RAIL_TIMEOUTAirwallex API slow or downRetry — Airwallex SLA is 99.9%
INSUFFICIENT_BALANCESource account balance lowTop up via Airwallex Dashboard
INVALID_BENEFICIARYBank account details rejectedVerify IBAN/account number format
FX_RATE_EXPIREDFX quote expired (>30s)Retry — SDK will re-fetch rate
COMPLIANCE_HOLDTransaction flagged for reviewContact Airwallex compliance team

Wise

ErrorCauseFix
RECIPIENT_REJECTEDWise rejected the recipient accountVerify bank details; use Wise’s validation API
TRANSFER_LIMIT_EXCEEDEDOver daily/monthly limitBreak into smaller transfers or contact Wise
PROFILE_NOT_VERIFIEDWise profile verification incompleteComplete KYB in Wise Dashboard

Stripe

ErrorCauseFix
CARD_DECLINEDCard network declined the chargeCheck card expiry, limit, 3DS requirements
PAYMENT_METHOD_NOT_FOUNDpm_... ID invalidRe-confirm payment method ID from Stripe Dashboard
STRIPE_WEBHOOK_FAILEDWebhook secret mismatchVerify STRIPE_WEBHOOK_SECRET env var

USDC on Base

ErrorCauseFix
TRANSACTION_REVERTEDUSDC transfer reverted on-chainCheck USDC balance and token allowance
GAS_ESTIMATION_FAILEDBase RPC returned errorCheck BASE_RPC_URL is reachable
NONCE_TOO_LOWTransaction nonce conflictWait for pending transactions to clear
RECIPIENT_REJECTEDInvalid 0x addressVerify address is checksummed and on Base

x402

ErrorCauseFix
X402_REQUIREMENTS_CHANGEDServer changed payment requirements mid-flowRe-probe with probeX402Endpoint()
FACILITATOR_UNREACHABLEx402.org/facilitator downCheck X402_FACILITATOR_URL or use self-hosted facilitator

Global retry strategy

import { executeAgentPayment, PQSafeError } from '@pqsafe/agent-pay'
const RETRYABLE_CODES = new Set([
'RAIL_TIMEOUT',
'FX_RATE_EXPIRED',
'NONCE_TOO_LOW',
])
async function executeWithRetry(signed: any, request: any, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await executeAgentPayment(signed, request)
} catch (err) {
if (err instanceof PQSafeError && RETRYABLE_CODES.has(err.code) && attempt < maxAttempts) {
const delayMs = Math.pow(2, attempt - 1) * 1000 // 1s, 2s, 4s
await new Promise(r => setTimeout(r, delayMs))
continue
}
throw err
}
}
}