Payment Status Polling
Overview
- Function: Best practices for polling payment intent status until completion
- Use Cases: Wait for payment completion, monitor payment progress, handle async payment flows
- Authentication: Optional (works with or without authentication)
Polling Strategies
1. Basic Fixed Interval Polling
Simple polling with fixed intervals. Suitable for short-duration operations.
async function pollStatus(intentId: string, intervalMs: number = 3000, maxAttempts: number = 60) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const intent = await client.getIntent(intentId);
// Terminal states - stop polling
if (intent.status === 'BASE_SETTLED') {
return { success: true, intent };
}
if (intent.status === 'EXPIRED' || intent.status === 'VERIFICATION_FAILED' || intent.status === 'PARTIAL_SETTLEMENT') {
return { success: false, intent, reason: intent.status };
}
// Wait before next poll
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error('Polling timeout - maximum attempts reached');
}
2. Exponential Backoff Polling
Reduces API calls over time. Recommended for longer operations.
async function pollWithExponentialBackoff(intentId: string) {
let delay = 2000; // Start with 2 seconds
const maxDelay = 30000; // Maximum 30 seconds
const maxDuration = 600000; // Maximum 10 minutes (intent expiration)
const startTime = Date.now();
while (true) {
// Check timeout
if (Date.now() - startTime > maxDuration) {
throw new Error('Polling timeout - exceeded maximum duration');
}
const intent = await client.getIntent(intentId);
// Terminal states - stop polling
if (intent.status === 'BASE_SETTLED') {
return { success: true, intent };
}
if (intent.status === 'EXPIRED' || intent.status === 'VERIFICATION_FAILED' || intent.status === 'PARTIAL_SETTLEMENT') {
return { success: false, intent, reason: intent.status };
}
// Exponential backoff: increase delay by 1.5x each time
await new Promise(resolve => setTimeout(resolve, delay));
delay = Math.min(delay * 1.5, maxDelay);
}
}
3. Adaptive Polling
Adjusts polling interval based on status progression.
async function adaptivePoll(intentId: string) {
let delay = 2000; // Initial delay: 2 seconds
while (true) {
const intent = await client.getIntent(intentId);
// Terminal states
if (intent.status === 'BASE_SETTLED') {
return { success: true, intent };
}
if (intent.status === 'EXPIRED' || intent.status === 'VERIFICATION_FAILED' || intent.status === 'PARTIAL_SETTLEMENT') {
return { success: false, intent, reason: intent.status };
}
// Adjust delay based on status
switch (intent.status) {
case 'AWAITING_PAYMENT':
delay = 5000; // Poll every 5 seconds while waiting
break;
case 'PENDING':
case 'SOURCE_SETTLED':
delay = 3000; // Poll every 3 seconds during processing
break;
case 'BASE_SETTLING':
delay = 2000; // Poll every 2 seconds during final settlement
break;
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
Go Implementation
Basic Polling
package main
import (
"context"
"fmt"
"time"
"github.com/cross402/usdc-sdk-go"
)
func pollStatus(ctx context.Context, client *pay.Client, intentID string, maxAttempts int) (*pay.Intent, error) {
for i := 0; i < maxAttempts; i++ {
intent, err := client.GetIntent(ctx, intentID)
if err != nil {
return nil, err
}
// Terminal states
switch intent.Status {
case pay.StatusBaseSettled:
return intent, nil
case pay.StatusExpired, pay.StatusVerificationFailed, pay.StatusPartialSettlement:
return intent, fmt.Errorf("payment failed: %s", intent.Status)
}
// Wait before next poll
time.Sleep(3 * time.Second)
}
return nil, fmt.Errorf("polling timeout - maximum attempts reached")
}
Exponential Backoff
func pollWithBackoff(ctx context.Context, client *pay.Client, intentID string) (*pay.Intent, error) {
delay := 2 * time.Second
maxDelay := 30 * time.Second
maxDuration := 10 * time.Minute
startTime := time.Now()
for {
// Check timeout
if time.Since(startTime) > maxDuration {
return nil, fmt.Errorf("polling timeout - exceeded maximum duration")
}
intent, err := client.GetIntent(ctx, intentID)
if err != nil {
return nil, err
}
// Terminal states
switch intent.Status {
case pay.StatusBaseSettled:
return intent, nil
case pay.StatusExpired, pay.StatusVerificationFailed, pay.StatusPartialSettlement:
return intent, fmt.Errorf("payment failed: %s", intent.Status)
}
// Exponential backoff
time.Sleep(delay)
delay = time.Duration(float64(delay) * 1.5)
if delay > maxDelay {
delay = maxDelay
}
}
}
Error Handling
Handle Rate Limiting
async function pollWithRateLimitHandling(intentId: string) {
let delay = 2000;
const maxDelay = 30000;
while (true) {
try {
const intent = await client.getIntent(intentId);
if (intent.status === 'BASE_SETTLED') {
return { success: true, intent };
}
if (intent.status === 'EXPIRED' || intent.status === 'VERIFICATION_FAILED' || intent.status === 'PARTIAL_SETTLEMENT') {
return { success: false, intent };
}
// Reset delay on success
delay = 2000;
await new Promise(resolve => setTimeout(resolve, delay));
} catch (error) {
if (error instanceof PayApiError && error.statusCode === 429) {
// Rate limited - increase delay
delay = Math.min(delay * 2, maxDelay);
console.log(`Rate limited, waiting ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
}
Retry on Network Errors
async function pollWithRetry(intentId: string, maxRetries: number = 3) {
let delay = 2000;
while (true) {
let retries = 0;
let intent;
// Retry logic for network errors
while (retries < maxRetries) {
try {
intent = await client.getIntent(intentId);
break; // Success
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw error;
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * retries));
}
}
// Check terminal states
if (intent.status === 'BASE_SETTLED') {
return { success: true, intent };
}
if (intent.status === 'EXPIRED' || intent.status === 'VERIFICATION_FAILED' || intent.status === 'PARTIAL_SETTLEMENT') {
return { success: false, intent };
}
await new Promise(resolve => setTimeout(resolve, delay));
delay = Math.min(delay * 1.5, 30000);
}
}
Best Practices
1. Polling Intervals
- Initial: 2-3 seconds for quick feedback
- Processing: 3-5 seconds during normal processing
- Final Settlement: 2-3 seconds when close to completion
- Maximum: 30 seconds to avoid excessive delays
2. Timeout Handling
- Set maximum polling duration (e.g., 10 minutes = intent expiration time)
- Stop polling immediately when terminal state is reached
- Provide user feedback on timeout
3. Rate Limiting
- Respect API rate limits (60 req/min/IP)
- Use exponential backoff to reduce API calls
- Handle HTTP 429 responses gracefully
4. User Experience
- Show loading indicators during polling
- Update UI with current status
- Provide clear error messages
- Allow user to cancel long-running polls
5. Error Recovery
- Retry on transient errors (network, 503)
- Handle rate limiting (429) with backoff
- Fail fast on terminal errors (404, 400)
Complete Example
import { PayClient, PayApiError } from '@cross402/usdc';
async function waitForPaymentCompletion(intentId: string): Promise<{
success: boolean;
intent: any;
transactionHash?: string;
}> {
const client = new PayClient({
baseUrl: 'https://api-pay.agent.tech',
auth: { apiKey: 'your-api-key', secretKey: 'your-secret-key' },
});
let delay = 2000;
const maxDelay = 30000;
const maxDuration = 600000; // 10 minutes
const startTime = Date.now();
while (true) {
// Check timeout
if (Date.now() - startTime > maxDuration) {
throw new Error('Payment polling timeout');
}
try {
const intent = await client.getIntent(intentId);
// Terminal states
if (intent.status === 'BASE_SETTLED') {
return {
success: true,
intent,
transactionHash: intent.basePayment?.txHash,
};
}
if (intent.status === 'EXPIRED' || intent.status === 'VERIFICATION_FAILED' || intent.status === 'PARTIAL_SETTLEMENT') {
return {
success: false,
intent,
};
}
// Reset delay on success
delay = 2000;
} catch (error) {
if (error instanceof PayApiError) {
if (error.statusCode === 429) {
// Rate limited - increase delay
delay = Math.min(delay * 2, maxDelay);
console.log(`Rate limited, waiting ${delay}ms`);
} else if (error.statusCode === 404) {
throw new Error('Intent not found');
} else {
throw error;
}
} else {
throw error;
}
}
// Wait before next poll
await new Promise(resolve => setTimeout(resolve, delay));
}
}