Error Handling
Every error response from the Burn & Claim API follows a consistent JSON format. This page covers the error shape, common error codes, and strategies for handling them.
Error response shape
interface ApiError {
statusCode: number
message: string
error?: string // HTTP status text
}Example:
{
"statusCode": 400,
"message": "items must contain at least 1 element",
"error": "Bad Request"
}HTTP status codes
| Status | Meaning | Retry? |
|---|---|---|
| 400 | Bad Request. The request body or query parameters are invalid. | No. Fix the request. |
| 401 | Unauthorized. API key is invalid/revoked, or session token is expired/malformed. | Recreate session; do not retry with same credentials. |
| 403 | Forbidden. The session token lacks the required scope for this endpoint. | No. Create a session with the correct scopes. |
| 429 | Too Many Requests. Rate limit or monthly quota exceeded. | Yes, after Retry-After seconds. |
| 500 | Internal Server Error. An unexpected server-side failure. | Yes, with exponential backoff. |
| 502 | Bad Gateway. Upstream service unavailable. | Yes, with exponential backoff. |
| 503 | Service Unavailable. The API is temporarily down. | Yes, with exponential backoff. |
Common error messages
Authentication
| Message | Cause | Fix |
|---|---|---|
Invalid API key | The X-Api-Key value is not recognized | Check the key in your dashboard |
API key revoked | The key was deleted from the dashboard | Create a new key |
Session expired | The session token’s TTL has elapsed | Create a new session from your backend |
Invalid session token | The token is malformed or was tampered with | Create a new session |
Wallet mismatch | The session token is bound to a different wallet | Create a session for the correct wallet |
Validation
| Message | Cause |
|---|---|
items must contain at least 1 element | The items array in buildTransactions is empty |
Invalid wallet address | The :address path parameter is not a valid Solana public key |
ttlSeconds must not be greater than 3600 | Session TTL exceeds the 1-hour maximum |
Unknown action for item type | The requested action is not valid for the item’s type |
Submission
| Code | Message | Fix |
|---|---|---|
TICKET_EXPIRED | The submission ticket has expired | Call buildTransactions again to get fresh tickets |
HASH_MISMATCH | The signed tx message doesn’t match the built tx | Sign the exact transaction returned by buildTransactions without modifying it |
RPC_ERROR | Solana RPC rejected the transaction | Check if the blockhash expired or if the wallet has enough SOL for fees |
Retry strategy
async function callWithRetry<T>(
fn: () => Promise<T>,
options: { maxRetries?: number; onRetry?: (attempt: number, delay: number) => void } = {}
): Promise<T> {
const { maxRetries = 3, onRetry } = options
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (err: unknown) {
const response = extractResponse(err)
if (!response) throw err
const status = response.status
// Never retry client errors (except 429)
if (status >= 400 && status < 500 && status !== 429) {
throw err
}
if (attempt === maxRetries) throw err
let delay: number
if (status === 429) {
const retryAfter = response.headers.get('Retry-After')
delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000
} else {
// Exponential backoff for 5xx: 1s, 2s, 4s...
delay = Math.min(1000 * 2 ** attempt, 15000)
}
// Add jitter
delay += Math.random() * 500
onRetry?.(attempt + 1, delay)
await new Promise((r) => setTimeout(r, delay))
}
}
throw new Error('Unreachable')
}
function extractResponse(err: unknown): Response | null {
if (err && typeof err === 'object' && 'response' in err) {
return (err as { response: Response }).response
}
return null
}Full error handling example
import { BurnerClient } from '@burnandclaim/sdk'
async function burnWalletAssets(
client: BurnerClient,
walletAddress: string
) {
// Step 1: Get assets (retryable)
const { items } = await callWithRetry(() =>
client.getWalletAssets(walletAddress)
)
if (items.length === 0) {
console.log('No burnable items found')
return
}
// Step 2: Build transactions (retryable)
const { transactions } = await callWithRetry(() =>
client.buildTransactions(walletAddress, {
items: items.map((item) => ({
type: item.type,
address: item.address,
action: item.actions[0],
})),
})
)
// Step 3: Sign (wallet adapter -- not retryable, user interaction)
const signed = await signTransactions(transactions)
// Step 4: Submit (retryable, idempotent via ticket dedup)
const { results } = await callWithRetry(() =>
client.submitTransactions(walletAddress, { signed })
)
// Step 5: Report results
for (const result of results) {
switch (result.status) {
case 'submitted':
console.log(`Burned: ${result.signature}`)
break
case 'duplicate':
console.log(`Already submitted: ${result.signature}`)
break
case 'failed':
console.error(`Failed: ${result.error?.code} -- ${result.error?.message}`)
break
}
}
}Last updated on