What This Document Covers:
- Practical integration patterns for LaunchAgencyBot Web UI API
- Error handling with retry logic and exponential backoff
- Cache control strategies for static vs dynamic data
- Session management and status polling for long-running operations
- Bulk operations with concurrency control
- Wallet validation and network selection patterns
- Production recommendations (authentication, rate limiting, error reporting)
Sections in This Document:
- Error Handling
- Cache Control
- Session Management
- Bulk Operations
- Wallet Validation
- Network Selection
- Pagination Strategies
- Production Recommendations
- WebSocket Integration (Future)
Related Documentation:
- → ../features/API_ENDPOINTS.md - Complete API endpoint reference
- → ./GETTING_STARTED.md - Setup and configuration guide
- → ../features/WEB_UI.md - Web UI architecture
Context Tags: #api #integration #patterns #best-practices #error-handling #production
Implement exponential backoff for 503 Service Unavailable responses:
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
// Retry on 503 (service temporarily unavailable)
if (response.status === 503 && attempt < maxRetries - 1) {
const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
await new Promise(r => setTimeout(r, delay));
continue;
}
throw new Error(error.detail || error.message);
}
return await response.json();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
// Wait before retry
const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
await new Promise(r => setTimeout(r, delay));
}
}
}FastAPI returns detailed validation errors with detail field. Check response.status === 400 for validation failures and display error.detail to users as validation feedback.
The API sets different cache policies for static vs dynamic data:
// Cache-first for static data (tags, configuration)
async function fetchWithCache(url, ttl = 3600) {
const cached = localStorage.getItem(url);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < ttl * 1000) {
return data;
}
}
const response = await fetch(url);
const data = await response.json();
localStorage.setItem(url, JSON.stringify({
data,
timestamp: Date.now()
}));
return data;
}
// Use cached tags
const tags = await fetchWithCache('/api/generation/tags', 3600);Always fetch fresh data for dynamic endpoints:
// Wallets, memecoins, trends should never be cached
const wallets = await fetch('/api/wallets', {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache'
}
});Long-running operations use session-based tracking:
async function startGeneration(request) {
// Start generation
const { session_id } = await fetch('/api/generation/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request)
}).then(r => r.json());
// Poll status until complete
return pollGenerationStatus(session_id);
}
async function pollGenerationStatus(sessionId, interval = 1000) {
while (true) {
const status = await fetch(`/api/generation/status/${sessionId}`)
.then(r => r.json());
if (status.status === 'completed') {
// Fetch final results
return fetch(`/api/generation/results/${sessionId}`)
.then(r => r.json());
}
if (status.status === 'failed') {
// Check retry availability
const canRetry = status.retry_count < 3;
if (canRetry) {
await fetch(`/api/generation/session/${sessionId}/retry`, {
method: 'POST'
});
// Continue polling
} else {
throw new Error(`Generation failed: ${status.error_message}`);
}
}
// Update UI with progress
updateProgress(status.progress, status.stage_description);
// Wait before next poll
await new Promise(r => setTimeout(r, interval));
}
}Sessions auto-expire after 1 hour. Clean up local state:
function cleanupCompletedSessions() {
const sessions = JSON.parse(localStorage.getItem('sessions') || '[]');
const now = Date.now();
const active = sessions.filter(s => {
const age = now - s.timestamp;
return age < 3600000; // 1 hour
});
localStorage.setItem('sessions', JSON.stringify(active));
}async function bulkApprove(tokenAddresses, onProgress) {
const results = [];
for (let i = 0; i < tokenAddresses.length; i++) {
const address = tokenAddresses[i];
try {
const result = await fetch(
`/api/memecoins/approve/${address}`,
{ method: 'POST' }
).then(r => r.json());
results.push({
address,
success: true,
message: result.message
});
} catch (error) {
results.push({
address,
success: false,
error: error.message
});
}
// Update progress
onProgress({
completed: i + 1,
total: tokenAddresses.length,
current: address,
results
});
// Rate limiting
if (i < tokenAddresses.length - 1) {
await new Promise(r => setTimeout(r, 100));
}
}
return results;
}async function parallelOperations(items, operation, concurrency = 5) {
const results = [];
const queue = [...items];
async function worker() {
while (queue.length > 0) {
const item = queue.shift();
if (!item) break;
try {
const result = await operation(item);
results.push({ item, success: true, result });
} catch (error) {
results.push({ item, success: false, error: error.message });
}
}
}
// Start workers
const workers = Array(concurrency).fill(null).map(() => worker());
await Promise.all(workers);
return results;
}
// Usage
const results = await parallelOperations(
tokenAddresses,
async (address) => {
const response = await fetch(`/api/memecoins/pending/${address}`);
return response.json();
},
5 // 5 concurrent requests
);Always validate wallet readiness before launches:
async function validateWalletForLaunch(walletAddress, requiredSol) {
// Refresh wallet from blockchain
const wallet = await fetch(`/api/wallets/${walletAddress}/refresh`, {
method: 'POST'
}).then(r => r.json());
// Check status
if (wallet.status !== 'FUNDED') {
throw new Error(
`Wallet not ready: status is ${wallet.status}. ` +
`Fund wallet with at least 0.25 SOL.`
);
}
// Check balance (with buffer for fees)
const required = requiredSol + 0.1; // +0.1 SOL for fees
if (wallet.sol_balance < required) {
throw new Error(
`Insufficient balance: has ${wallet.sol_balance} SOL, ` +
`needs ${required} SOL (${requiredSol} + 0.1 fees)`
);
}
return wallet;
}Always specify network for blockchain operations:
const network = process.env.NODE_ENV === 'production'
? 'mainnet'
: 'devnet';
async function launchToken(request) {
// Development - use devnet
const response = await fetch(
`/api/launches?network=${network}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request)
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail);
}
return response.json();
}class PaginatedFetcher {
constructor(endpoint, pageSize = 20) {
this.endpoint = endpoint;
this.pageSize = pageSize;
this.currentPage = 1;
this.hasMore = true;
this.items = [];
}
async fetchNext() {
if (!this.hasMore) return [];
const url = `${this.endpoint}?page=${this.currentPage}&limit=${this.pageSize}`;
const response = await fetch(url).then(r => r.json());
this.items.push(...response.memecoins);
this.hasMore = response.has_next;
this.currentPage++;
return response.memecoins;
}
reset() {
this.currentPage = 1;
this.hasMore = true;
this.items = [];
}
}
// Usage
const fetcher = new PaginatedFetcher('/api/memecoins/pending', 50);
const firstPage = await fetcher.fetchNext();Implement JWT-based authentication for production:
// Add JWT token to requests
async function authenticatedFetch(url, options = {}) {
const token = localStorage.getItem('jwt_token');
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
}Implement client-side rate limiting:
class RateLimiter {
constructor(maxRequests = 10, windowMs = 1000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = [];
}
async throttle() {
const now = Date.now();
this.requests = this.requests.filter(t => now - t < this.windowMs);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.windowMs - (now - oldestRequest);
await new Promise(r => setTimeout(r, waitTime));
}
this.requests.push(Date.now());
}
}
const limiter = new RateLimiter(10, 1000); // 10 req/sec
async function makeRequest(url) {
await limiter.throttle();
return fetch(url);
}Integrate error tracking for production:
async function apiRequest(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
// Report to error tracking service
reportError({
type: 'API_ERROR',
url,
status: response.status,
message: error.detail,
timestamp: new Date().toISOString()
});
throw new Error(error.detail);
}
return response.json();
} catch (error) {
// Report network errors
reportError({
type: 'NETWORK_ERROR',
url,
message: error.message,
timestamp: new Date().toISOString()
});
throw error;
}
}Set reasonable timeouts for all requests:
async function fetchWithTimeout(url, options = {}, timeout = 30000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
}
}For future WebSocket support (trends, generation status):
class RealtimeUpdates {
constructor(url) {
this.url = url;
this.ws = null;
this.listeners = new Map();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const listeners = this.listeners.get(data.type) || [];
listeners.forEach(callback => callback(data));
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
// Reconnect with exponential backoff
};
}
subscribe(type, callback) {
if (!this.listeners.has(type)) {
this.listeners.set(type, []);
}
this.listeners.get(type).push(callback);
}
unsubscribe(type, callback) {
const listeners = this.listeners.get(type) || [];
const index = listeners.indexOf(callback);
if (index > -1) listeners.splice(index, 1);
}
}Last Updated: January 2025 - LaunchAgencyBot Web UI v2.0