Skip to content
Launch App >

Error responses from the Turbine.cash relayer and indexer API.

CodeMeaning
200Success
400Bad Request - Invalid input
401Unauthorized - Invalid/expired token
403Forbidden - OFAC or permission denied
404Not Found - Resource doesn’t exist
429Too Many Requests - Rate limited
500Internal Server Error
503Service Unavailable

Most errors return JSON:

{
"status": 400,
"error": "ErrorCode",
"message": "Human-readable description"
}

Or for relay endpoints:

{
"status": 400,
"error": "NullifierAlreadySpent"
}
{
"status": 400,
"error": "InvalidSignature",
"message": "Signature does not match wallet and message"
}

Causes:

  • Wrong wallet signed the message
  • Message was modified after signing
  • Signature encoding is incorrect

Solution: Ensure the signature is for the exact timestamp message.

{
"status": 401,
"error": "TokenExpired",
"message": "Auth token has expired"
}

Solution: Re-authenticate with a fresh signature.

{
"status": 401,
"error": "InvalidToken",
"message": "Auth token is invalid or malformed"
}

Solution: Check token format and re-authenticate.

{
"status": 400,
"error": "NullifierAlreadySpent"
}

Already withdrawn. Check local records.

{
"status": 400,
"error": "InvalidProof"
}

ZK proof failed verification. Regenerate with correct inputs.

{
"status": 400,
"error": "InvalidRoot"
}

Merkle root is stale. Refetch and regenerate proof.

{
"status": 403,
"error": "OFACCheckFailed"
}

Recipient address failed compliance check.

{
"status": 500,
"error": "RelayFailed",
"message": "Transaction submission failed: ..."
}

Relay couldn’t submit transaction. May be temporary - retry.

{
"status": 400,
"error": "SwapFailed",
"message": "Jupiter swap failed: insufficient liquidity"
}

Jupiter couldn’t execute swap. Try different amount or route.

{
"status": 400,
"error": "SlippageExceeded",
"message": "Price moved beyond slippage tolerance"
}

Increase slippage or retry when markets stabilize.

{
"status": 429,
"error": "RateLimited",
"message": "Too many requests. Retry after 60 seconds"
}

Wait before retrying. Consider implementing backoff.

interface ApiError {
status: number;
error: string;
message?: string;
}
async function callApi(endpoint: string, options: RequestInit): Promise<any> {
const response = await fetch(endpoint, options);
if (!response.ok) {
let error: ApiError;
try {
error = await response.json();
} catch {
error = {
status: response.status,
error: 'UnknownError',
message: await response.text(),
};
}
throw new ApiException(error);
}
return response.json();
}
class ApiException extends Error {
constructor(public error: ApiError) {
super(error.message || error.error);
}
isRetryable(): boolean {
// Retry on rate limits and server errors
return this.error.status === 429 || this.error.status >= 500;
}
isAuthError(): boolean {
return this.error.status === 401;
}
}
// Usage with retry logic
async function relayWithRetry(params: RelayParams, maxRetries: number = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await callApi('/relay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
} catch (err) {
if (err instanceof ApiException) {
if (err.isAuthError()) {
// Re-authenticate
await reAuthenticate();
continue;
}
if (err.isRetryable() && attempt < maxRetries - 1) {
await sleep(1000 * (attempt + 1)); // Exponential backoff
continue;
}
}
throw err;
}
}
}
  1. Check request format:

    console.log('Request:', JSON.stringify(body, null, 2));
  2. Verify auth token:

    const isValid = await fetch(`/is_logged/${token}`);
    console.log('Token valid:', isValid.ok);
  3. Check API health:

    const health = await fetch('/health');
    console.log('API healthy:', health.ok);
  4. Review response headers:

    response.headers.forEach((value, key) => {
    console.log(`${key}: ${value}`);
    });