Error Handling Guide
A comprehensive guide to handling errors, edge cases, and partial failures when using the Kixago API.
Overview
The Kixago API uses standard HTTP status codes and JSON error responses. Understanding how to handle errors properly is critical for building robust applications.
Error Philosophy
- ✅ Partial failures are non-fatal - One protocol failing doesn't break the entire response
- ✅ Always return valid JSON - Even error responses are well-structured
- ✅ Descriptive error messages - Clear explanations of what went wrong
- ✅ Retry-friendly - Transient errors (429, 500) can be retried
HTTP Status Codes
Success Codes
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request successful, profile returned |
Client Error Codes (4xx)
| Code | Status | Description | Retry? |
|---|---|---|---|
| 400 | Bad Request | Invalid wallet address or malformed request | ❌ No |
| 401 | Unauthorized | Missing or invalid API key | ❌ No |
| 404 | Not Found | Endpoint doesn't exist | ❌ No |
| 429 | Too Many Requests | Rate limit exceeded | ✅ Yes (after delay) |
Server Error Codes (5xx)
| Code | Status | Description | Retry? |
|---|---|---|---|
| 500 | Internal Server Error | Unexpected server error | ✅ Yes (with backoff) |
| 502 | Bad Gateway | Upstream service error | ✅ Yes (with backoff) |
| 503 | Service Unavailable | Temporary outage | ✅ Yes (with backoff) |
| 504 | Gateway Timeout | Request took too long | ✅ Yes (once) |
Error Response Format
All error responses follow this structure:
{
"error": "Human-readable error message"
}
Example:
{
"error": "invalid address format - use hex address or ENS name"
}
Common Error Scenarios
Error 1: Invalid API Key (401)
Request:
curl "https://api.kixago.com/v1/risk-profile/0xf0bb..."
# (no API key header)
Response:
{
"error": "missing or invalid API key"
}
Causes:
- ❌ No
X-API-Keyheader - ❌ Wrong API key
- ❌ API key revoked
- ❌ API key expired (if expiration set)
Solution:
// ✅ Correct
const response = await fetch(url, {
headers: {
'X-API-Key': process.env.KIXAGO_API_KEY
}
});
// ❌ Wrong - missing header
const response = await fetch(url);
Error 2: Invalid Wallet Address (400)
Request:
curl -H "X-API-Key: key" \
"https://api.kixago.com/v1/risk-profile/invalid"
Response:
{
"error": "invalid address format - use hex address or ENS name"
}
Causes:
- ❌ Address too short/long
- ❌ Missing
0xprefix - ❌ Invalid characters
- ❌ Not a valid ENS name
Valid formats:
✅ 0xf0bb20865277aBd641a307eCe5Ee04E79073416C (42 chars, 0x prefix)
✅ vitalik.eth (valid ENS name)
❌ f0bb20865277aBd641a307eCe5Ee04E79073416C (missing 0x)
❌ 0xf0bb (too short)
❌ random-string (invalid)
Validation example:
function isValidAddress(address: string): boolean {
// Hex address: 0x + 40 hex characters
if (/^0x[a-fA-F0-9]{40}$/.test(address)) {
return true;
}
// ENS name: ends with .eth
if (address.endsWith('.eth')) {
return true;
}
return false;
}
if (!isValidAddress(walletAddress)) {
throw new Error('Invalid wallet address format');
}
Error 3: ENS Resolution Failure (400)
Request:
curl -H "X-API-Key: key" \
"https://api.kixago.com/v1/risk-profile/nonexistent.eth"
Response:
{
"error": "could not resolve ENS name: nonexistent.eth"
}
Causes:
- ❌ ENS name doesn't exist
- ❌ ENS name expired
- ❌ ENS resolver error
Solution:
try {
const profile = await getRiskProfile('nonexistent.eth');
} catch (err) {
if (err.message.includes('could not resolve ENS')) {
// Handle ENS resolution failure
console.error('ENS name not found');
}
}
Error 4: Rate Limit Exceeded (429)
Request:
# (after exceeding rate limit)
curl -H "X-API-Key: key" \
"https://api.kixago.com/v1/risk-profile/0xf0bb..."
Response:
{
"error": "rate limit exceeded",
"retry_after": 5
}
Headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705334405
Retry-After: 5
Rate limits by plan:
| Plan | Requests/Month | Rate Limit | Burst |
|---|---|---|---|
| Developer (Free) | 10,000 | 10 req/sec | 20 req/sec |
| Startup | 100,000 | 50 req/sec | 100 req/sec |
| Institution | 1,000,000 | 200 req/sec | 400 req/sec |
Handling rate limits:
async function fetchWithRateLimit(url: string) {
const response = await fetch(url, {
headers: { 'X-API-Key': apiKey }
});
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '5');
console.log(`Rate limited - waiting ${retryAfter}s...`);
await sleep(retryAfter * 1000);
// Retry once
return fetch(url, {
headers: { 'X-API-Key': apiKey }
});
}
return response;
}
Error 5: Internal Server Error (500)
Request:
curl -H "X-API-Key: key" \
"https://api.kixago.com/v1/risk-profile/0xf0bb..."
Response:
{
"error": "Error generating portfolio risk"
}
Causes:
- ❌ Unexpected server error
- ❌ Database error
- ❌ Upstream RPC node failure
Solution:
async function fetchWithRetry(url: string, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, {
headers: { 'X-API-Key': apiKey }
});
if (response.status === 500 && i < maxRetries - 1) {
// Retry on 500 errors
const delay = 1000 * Math.pow(2, i); // Exponential backoff: 1s, 2s, 4s
console.log(`Server error - retrying in ${delay}ms...`);
await sleep(delay);
continue;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (err) {
if (i === maxRetries - 1) throw err;
}
}
}
Partial Failures (Non-Fatal)
Understanding aggregation_errors
The Kixago API queries multiple protocols across multiple chains. If one protocol fails, the API doesn't fail the entire request - it returns data from successful protocols.
Example response with partial failure:
{
"wallet_address": "0xf0bb20865277aBd641a307eCe5Ee04E79073416C",
"total_collateral_usd": 2139957718.47,
"lending_positions": [
{
"protocol": "Aave",
"chain": "Ethereum",
"collateral_usd": 2077178086.09
},
{
"protocol": "Aave",
"chain": "Base",
"collateral_usd": 62779632.38
}
],
"aggregation_errors": {
"CompoundV3:Ethereum": "could not get numAssets from Compound V3 market"
}
}
What happened:
- ✅ Aave V3 on Ethereum: Success
- ✅ Aave V3 on Base: Success
- ❌ Compound V3 on Ethereum: Failed (but didn't break the request)
Common Aggregation Errors
| Protocol Key | Error Message | Cause | Impact |
|---|---|---|---|
CompoundV3:Ethereum | "could not get numAssets from Compound V3 market" | RPC error or contract issue | Missing Compound positions |
Aave:Ethereum | "max retries exceeded: 429 Too Many Requests" | Alchemy rate limit | Missing Aave positions |
MakerDAO:Ethereum | "timeout waiting for response" | RPC timeout | Missing MakerDAO vaults |
Handling Aggregation Errors
Strategy 1: Log and Continue (Recommended)
const profile = await getRiskProfile(address);
if (profile.aggregation_errors && Object.keys(profile.aggregation_errors).length > 0) {
// Log warnings but continue
console.warn('⚠️ Some protocols failed:', profile.aggregation_errors);
// Optional: Send to error tracking (Sentry, etc.)
Sentry.captureMessage('Partial DeFi aggregation failure', {
extra: { errors: profile.aggregation_errors }
});
}
// Use the data you have
console.log(`Found ${profile.lending_positions.length} positions`);
Strategy 2: Retry Failed Protocols
const profile = await getRiskProfile(address);
if (profile.aggregation_errors && Object.keys(profile.aggregation_errors).length > 0) {
console.warn('Some protocols failed - retrying in 5s...');
// Wait and retry
await sleep(5000);
const retried = await getRiskProfile(address);
if (!retried.aggregation_errors || Object.keys(retried.aggregation_errors).length === 0) {
console.log('✅ Retry successful - all protocols now working');
return retried;
}
}
return profile; // Use partial data
Strategy 3: Fail if Critical Protocols Missing
const profile = await getRiskProfile(address);
// Define critical protocols for your use case
const criticalProtocols = ['Aave:Ethereum', 'Compound:Ethereum'];
if (profile.aggregation_errors) {
const failedCritical = criticalProtocols.some(protocol =>
Object.keys(profile.aggregation_errors).some(key => key.includes(protocol))
);
if (failedCritical) {
throw new Error('Critical protocols failed - cannot proceed with underwriting');
}
}
// Safe to proceed
return profile;
Strategy 4: Display Warning to Users
function DisplayPortfolio({ profile }) {
const hasPartialFailure = profile.aggregation_errors &&
Object.keys(profile.aggregation_errors).length > 0;
return (
<div>
{hasPartialFailure && (
<Alert severity="warning">
⚠️ Some DeFi protocols are temporarily unavailable.
The data shown may be incomplete.
<details>
<summary>Details</summary>
<pre>{JSON.stringify(profile.aggregation_errors, null, 2)}</pre>
</details>
</Alert>
)}
<PortfolioSummary data={profile} />
</div>
);
}
Network Errors
Timeout Errors
Symptom: Request takes longer than client timeout
// Set appropriate timeout (API can take 1-5 seconds)
const response = await fetch(url, {
headers: { 'X-API-Key': apiKey },
signal: AbortSignal.timeout(30000) // 30 second timeout
});
Recommended timeouts:
- Client timeout: 30 seconds (to account for slow RPC nodes)
- Server timeout: API has internal 30s timeout
- Cache hit: < 100ms
Connection Errors
Symptom: Network unreachable, DNS failure, etc.
try {
const profile = await getRiskProfile(address);
} catch (err) {
if (err.name === 'TypeError' && err.message.includes('fetch')) {
// Network error
console.error('Network error - check your connection');
} else if (err.code === 'ECONNREFUSED') {
// Connection refused
console.error('Cannot connect to API - service may be down');
} else {
throw err;
}
}
Edge Cases
Case 1: Wallet with No DeFi Positions
Response:
{
"wallet_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"total_collateral_usd": 0,
"total_borrowed_usd": 0,
"global_health_factor": 0,
"lending_positions": [],
"defi_score": null
}
Key indicator: defi_score is null
Handling:
const profile = await getRiskProfile(address);
if (!profile.defi_score) {
return {
message: 'This wallet has no DeFi lending positions',
recommendation: 'No credit history available'
};
}
// Safe to access score now
const score = profile.defi_score.defi_score;
Case 2: Null Token Details
Sometimes collateral_details or borrowed_details may be null:
{
"protocol": "Aave",
"chain": "Ethereum",
"collateral_usd": 2077178086.09,
"collateral_details": null,
"borrowed_details": [...]
}
Handling:
position.collateral_details?.forEach(detail => {
// Safe - only runs if collateral_details is not null
console.log(detail.token);
});
// Or with explicit check
if (position.collateral_details) {
for (const detail of position.collateral_details) {
console.log(detail.token);
}
}
Case 3: Extreme Values
Some wallets have extremely large positions (billions of dollars):
{
"total_collateral_usd": 2139957718.47
}
Handling in JavaScript:
// ❌ Wrong - precision loss
const collateral = 2139957718.47;
console.log(collateral); // May lose precision
// ✅ Correct - use BigInt for large integers or format for display
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(collateral);
console.log(formatted); // "$2,139,957,718"
Complete Error Handling Example
TypeScript with Full Error Handling
// error-handling.ts
import fetch from 'node-fetch';
class KixagoAPIError extends Error {
constructor(
message: string,
public statusCode?: number,
public retryable: boolean = false
) {
super(message);
this.name = 'KixagoAPIError';
}
}
async function getRiskProfileSafe(
walletAddress: string,
maxRetries = 3
): Promise<RiskProfileResponse> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(
`https://api.kixago.com/v1/risk-profile/${walletAddress}`,
{
headers: {
'X-API-Key': process.env.KIXAGO_API_KEY!
},
signal: AbortSignal.timeout(30000)
}
);
// Handle specific HTTP errors
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.error || `HTTP ${response.status}`;
switch (response.status) {
case 400:
throw new KixagoAPIError(
`Invalid request: ${errorMessage}`,
400,
false // Don't retry
);
case 401:
throw new KixagoAPIError(
'Invalid API key',
401,
false // Don't retry
);
case 429:
const retryAfter = parseInt(response.headers.get('Retry-After') || '5');
if (attempt < maxRetries - 1) {
console.log(`Rate limited - waiting ${retryAfter}s...`);
await sleep(retryAfter * 1000);
continue; // Retry
}
throw new KixagoAPIError(
'Rate limit exceeded',
429,
true // Retryable
);
case 500:
case 502:
case 503:
if (attempt < maxRetries - 1) {
const delay = 1000 * Math.pow(2, attempt); // Exponential backoff
console.log(`Server error - retrying in ${delay}ms...`);
await sleep(delay);
continue; // Retry
}
throw new KixagoAPIError(
'Server error',
response.status,
true // Retryable
);
default:
throw new KixagoAPIError(
errorMessage,
response.status,
false
);
}
}
// Parse successful response
const data = await response.json();
// Warn about partial failures
if (data.aggregation_errors && Object.keys(data.aggregation_errors).length > 0) {
console.warn('⚠️ Partial failure:', data.aggregation_errors);
}
return data;
} catch (err) {
lastError = err as Error;
// Don't retry non-retryable errors
if (err instanceof KixagoAPIError && !err.retryable) {
throw err;
}
// Network errors - retry
if (err instanceof TypeError) {
if (attempt < maxRetries - 1) {
const delay = 1000 * Math.pow(2, attempt);
console.log(`Network error - retrying in ${delay}ms...`);
await sleep(delay);
continue;
}
}
// Last attempt - throw
if (attempt === maxRetries - 1) {
throw lastError;
}
}
}
throw lastError || new Error('Max retries exceeded');
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Usage
try {
const profile = await getRiskProfileSafe('0xf0bb20865277aBd641a307eCe5Ee04E79073416C');
if (!profile.defi_score) {
console.log('No DeFi positions found');
} else {
console.log(`DeFi Score: ${profile.defi_score.defi_score}`);
}
} catch (err) {
if (err instanceof KixagoAPIError) {
console.error(`API Error (${err.statusCode}): ${err.message}`);
if (err.retryable) {
console.log('You can retry this request');
}
} else {
console.error('Unexpected error:', err);
}
}
Best Practices Checklist
✅ DO
- Validate inputs before sending requests
- Set appropriate timeouts (30 seconds recommended)
- Implement retry logic for 429, 500, 502, 503 errors
- Use exponential backoff when retrying
- Check
aggregation_errorsfor partial failures - Log errors for debugging and monitoring
- Handle
defi_score: nullfor wallets with no positions - Use optional chaining for nullable fields (
?.) - Cache responses to reduce API calls
- Monitor rate limit headers to avoid 429 errors
❌ DON'T
- Don't retry 400, 401, 404 errors - they won't succeed
- Don't retry infinitely - use max retry limit (3-5)
- Don't ignore partial failures - log
aggregation_errors - Don't fail on partial data - use what you have
- Don't hardcode delays - use
Retry-Afterheader - Don't expose API keys in errors - sanitize error messages
- Don't assume all fields exist - validate before accessing
- Don't spam the API - respect rate limits
Monitoring and Alerting
What to Monitor
// Track error rates
function trackError(error: Error, context: any) {
// Send to monitoring service (Sentry, Datadog, etc.)
Sentry.captureException(error, {
tags: {
api: 'kixago',
endpoint: 'risk-profile'
},
extra: context
});
// Track metrics
metrics.increment('kixago.errors', {
type: error.name,
statusCode: (error as any).statusCode
});
}
// Track partial failures
function trackPartialFailure(errors: Record<string, string>) {
metrics.increment('kixago.partial_failures', {
protocols: Object.keys(errors).join(',')
});
}
// Track success rate
function trackSuccess(duration: number, cached: boolean) {
metrics.timing('kixago.response_time', duration);
metrics.increment('kixago.success', { cached });
}
Alerting Thresholds
Set up alerts for:
- Error rate > 5% over 5 minutes
- Partial failure rate > 20% over 5 minutes
- 429 errors > 10 per minute (approaching rate limit)
- Average response time > 10 seconds
- No successful requests for 5 minutes
Next Steps
Need Help?
- Persistent errors? Email [email protected] with request details
- Rate limit issues? Upgrade your plan
- Found a bug? Report it
---