Skip to content

Errors & rate limits

This page describes how the Qeychain v1 API reports failures, the request limits it enforces, how your plan tier caps returned data, and the signals it uses when a response is partial or truncated. All paths are under https://api.qeychain.xyz/api/v1.

Errors use the same envelope as successful responses, with success: false and an error object in place of data:

{
"success": false,
"error": {
"code": "INVALID_ADDRESS",
"message": "Invalid wallet address. Supported: Ethereum (0x...), Bitcoin (bc1... / 1... / 3...), Solana.",
"details": { "provided": "0xnotanaddress" },
"field": "address"
},
"meta": {
"request_id": "5f2b0c9e-1d7a-4a3e-9b2c-0a1b2c3d4e5f",
"timestamp": "2026-02-19T15:04:05.123Z",
"version": "1.0.0"
}
}
  • error.code — a stable, machine-readable code you can branch on.
  • error.message — a human-readable description.
  • error.details — optional structured context (present on some errors).
  • error.field — optional; the offending field on validation errors.
  • meta.request_id — quote this when contacting support about a failed call.

Always branch on error.code and the HTTP status, not on error.message (message text may change).

HTTP error.code Meaning
400 INVALID_ADDRESS The wallet address is not a valid EVM, Bitcoin, or Solana address.
400 VALIDATION_ERROR A query parameter or body field failed validation (see error.field).
400 INVALID_REQUEST The request was malformed.
401 UNAUTHORIZED No valid authentication was provided, or the bearer token is invalid or expired.
403 FORBIDDEN Authenticated, but not permitted to access the resource.
404 WALLET_NOT_FOUND The wallet has not been indexed yet. Load its dashboard first.
404 RESOURCE_NOT_FOUND / LOT_NOT_FOUND The requested resource (e.g. a symbol or lot) does not exist.
429 RATE_LIMIT_EXCEEDED You exceeded a per-minute or per-day request limit.
500 INTERNAL_ERROR / CONFIGURATION_ERROR An unexpected server error. Safe to retry later.
502 DATA_PROVIDER_ERROR An upstream data provider is down or failing. Retry later.
502 PROVIDER_ENTITLEMENT_ERROR An upstream provider rejected the request at the plan/quota level.
503 SERVICE_UNAVAILABLE A dependency is temporarily unavailable.

Requests are metered per client on two windows:

Window Setting Default
Per minute RATE_LIMIT_RPM 60 requests / minute
Per day RATE_LIMIT_RPD 1,000 requests / day

The per-day limit exists partly to protect shared upstream data-provider quotas. When you cross either threshold, the API returns HTTP 429 with a Retry-After header and, depending on which limit tripped, X-RateLimit-* headers describing your remaining budget:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit-Minute: 60
X-RateLimit-Limit-Day: 1000
X-RateLimit-Remaining-Minute: 0
X-RateLimit-Remaining-Day: 742
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Maximum 60 requests per minute.",
"details": { "limit": 60, "window": "minute", "retry_after_seconds": 60 }
},
"meta": {
"request_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f",
"timestamp": "2026-02-19T15:05:00.000Z",
"version": "1.0.0"
}
}

When you receive a 429, wait at least Retry-After seconds before retrying. The per-minute window is a rolling 60-second window; the per-day counter resets at midnight UTC.

Your account’s plan tier sets a transaction cap — the maximum number of (most recent) transactions reflected in a response — and a cap on how many wallets you can link:

Plan (plan_name) Label Transaction cap Linked-wallet cap
free Free 25 1
basic Starter 100 5
professional Professional 5,000 25
defi DeFi Power User Unlimited Unlimited

First-party gateway and partner integrations resolve to unlimited.

How the cap is applied. The cost-basis engine always processes a wallet’s full history so FIFO cost basis stays correct. The cap is applied to the response: only disposals, lots, and transactions from the most recent N transactions (where N is your cap) are returned. On the dashboard endpoint, three fields tell you whether truncation occurred:

  • is_limitedtrue when the response was capped by your plan tier.
  • tx_limit — your plan’s cap (null for unlimited).
  • tx_total — the true total transaction count (present when is_limited is true).

Qeychain sources on-chain data from upstream providers. Two distinct 502 failures can surface, and they mean different things:

  • DATA_PROVIDER_ERROR — a genuine provider outage: the provider is down, timing out, or returning server errors. Retrying later is appropriate.

    {
    "success": false,
    "error": {
    "code": "DATA_PROVIDER_ERROR",
    "message": "Failed to fetch data from data provider. Please try again later.",
    "details": { "provider": "data provider", "error": "upstream timeout" }
    },
    "meta": { "request_id": "...", "timestamp": "...", "version": "1.0.0" }
    }
  • PROVIDER_ENTITLEMENT_ERROR — the provider authenticated the request but rejected it at the plan/entitlement level (an expired trial or an exhausted quota). The provider itself is up; this is an account/billing condition, so blindly retrying will not help.

    {
    "success": false,
    "error": {
    "code": "PROVIDER_ENTITLEMENT_ERROR",
    "message": "Data provider Allium rejected the request due to a plan/entitlement limitation (expired trial or exhausted quota). This is a provider account issue, not an outage.",
    "details": { "provider": "Allium", "error": "User's trial has ended and is no longer able to access this resource." }
    },
    "meta": { "request_id": "...", "timestamp": "...", "version": "1.0.0" }
    }

Both use HTTP 502 (the status contract is identical); branch on error.code to distinguish an outage from an entitlement problem.

Beyond plan caps, three mechanisms signal that a response is not the complete picture.

GET /wallets/{address}/holdings and .../holdings/summary return two boolean flags alongside the payload:

  • partialtrue when cost-basis enrichment did not finish within the request’s time budget.
  • still_indexing — mirrors partial.

When these are true, the balances and current values are complete and accurate, but cost_basis and unrealized_gl are still being computed in the background (this happens on a cold cache for very active wallets). The background computation warms the cache, so retrying the request shortly resolves to full cost-basis data. Partial responses are intentionally not cached, so a retry always upgrades to the complete result.

{
"success": true,
"data": [ /* holdings with complete balances */ ],
"meta": { "request_id": "...", "timestamp": "...", "version": "1.0.0" },
"pagination": { "page": 1, "per_page": 50, "total": 8, "total_pages": 1, "has_more": false },
"partial": true,
"still_indexing": true
}

GET /wallets/{address}/transactions/export (CSV or JSON) returns two response headers so a download can flag incompleteness instead of silently cutting off:

  • X-Export-Count — the number of rows in the export.
  • X-Export-Truncatedtrue if the upstream provider hit its page cap and the history is incomplete, otherwise false.

Both headers are exposed via CORS. The JSON export body additionally includes count and truncated fields:

{
"transactions": [ /* ... */ ],
"count": 5000,
"truncated": true
}

Heavy endpoints (/dashboard, /transactions, /cost-basis) run under a longer wall-clock ceiling than lighter ones. If a request exceeds its budget, the API returns HTTP 504 with a plain body:

{ "error": "Request timeout" }

The first call for a large, un-cached wallet is the most likely to hit this; because partial work warms the cache, a retry usually succeeds and returns faster.