Best Practices
Tips for building reliable, user-friendly X402 resources.
Reliability
Handle Failures Gracefully
try {
const result = await processRequest(input);
return { success: true, data: result };
} catch (error) {
console.error('Processing error:', error);
return {
state: 'failed',
error: 'Processing failed. Please try again.',
code: 'processing_error'
};
}
Set Reasonable Timeouts
Don't let operations run forever:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
try {
const result = await fetch(url, { signal: controller.signal });
clearTimeout(timeout);
return result;
} catch (error) {
if (error.name === 'AbortError') {
return { state: 'failed', error: 'Request timed out', code: 'timeout' };
}
throw error;
}
Implement Idempotency
Allow safe retries by using idempotency keys:
app.post('/generate', async (req, res) => {
const idempotencyKey = req.headers['x-idempotency-key'];
if (idempotencyKey) {
const existing = await db.jobs.findByIdempotencyKey(idempotencyKey);
if (existing) {
return res.json({ jobId: existing.id, statusUrl: ... });
}
}
// Create new job...
});
User Experience
Provide Helpful Descriptions
{
"prompt": {
"type": "string",
"required": true,
"description": "Describe the image you want. Be specific about style, colors, and composition."
}
}
Use Sensible Defaults
{
"quality": {
"type": "string",
"enum": ["draft", "standard", "high"],
"default": "standard",
"description": "Higher quality = longer generation time"
}
}
Show Progress for Long Operations
// Update progress during processing
async function processWithProgress(jobId, steps) {
for (let i = 0; i < steps.length; i++) {
await db.jobs.update(jobId, {
progress: Math.floor((i / steps.length) * 100)
});
await steps[i]();
}
}
Performance
Cache When Possible
const cache = new Map();
app.get('/status/:jobId', async (req, res) => {
const cached = cache.get(req.params.jobId);
if (cached && cached.state === 'succeeded') {
return res.json(cached);
}
const result = await db.jobs.findById(req.params.jobId);
if (result.state === 'succeeded') {
cache.set(req.params.jobId, result);
}
return res.json(result);
});
Clean Up Old Data
// Run daily cleanup
async function cleanupOldJobs() {
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
await db.jobs.deleteWhere({ createdAt: { lt: cutoff } });
}
Security
Validate All Inputs
import { z } from 'zod';
const inputSchema = z.object({
prompt: z.string().min(1).max(1000),
style: z.enum(['realistic', 'anime', 'cartoon']).optional(),
});
app.post('/generate', async (req, res) => {
const parsed = inputSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: 'Invalid input',
details: parsed.error.issues
});
}
// Use parsed.data...
});
Sanitize Outputs
Don't expose internal details in error messages:
// Bad
return { error: `Database error: ${error.message}` };
// Good
console.error('Database error:', error);
return { error: 'An error occurred. Please try again.', code: 'internal_error' };
Pricing
Set Fair Prices
Consider:
- Your compute costs
- API costs (OpenAI, etc.)
- Storage costs
- Fair margin
Be Transparent
Include pricing in your 402 response:
{
"maxAmountRequired": "100000",
"extra": {
"pricing": {
"amount": 0.10,
"currency": "USDC",
"description": "Per image generation"
}
}
}
Monitoring
Log Important Events
console.log(`[${jobId}] Job started`, { input: sanitizedInput });
console.log(`[${jobId}] Job completed`, { duration: Date.now() - startTime });
console.error(`[${jobId}] Job failed`, { error: error.message });
Track Metrics
- Success rate
- Average processing time
- Error distribution
- Usage patterns
Testing
Test the Full Flow
- 402 response format
- Payment verification
- Job creation
- Status polling
- Success/failure states
Test Edge Cases
- Invalid inputs
- Timeouts
- Concurrent requests
- Payment failures