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.
The error envelope
Section titled “The error envelope”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 status codes
Section titled “HTTP status codes”| 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. |
Rate limits
Section titled “Rate limits”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 RequestsRetry-After: 60X-RateLimit-Limit-Minute: 60X-RateLimit-Limit-Day: 1000X-RateLimit-Remaining-Minute: 0X-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.
Plan tiers and transaction caps
Section titled “Plan tiers and transaction caps”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_limited—truewhen the response was capped by your plan tier.tx_limit— your plan’s cap (nullfor unlimited).tx_total— the true total transaction count (present whenis_limitedistrue).
Provider errors (502)
Section titled “Provider errors (502)”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.
Partial and truncated results
Section titled “Partial and truncated results”Beyond plan caps, three mechanisms signal that a response is not the complete picture.
Holdings still indexing
Section titled “Holdings still indexing”GET /wallets/{address}/holdings and .../holdings/summary return two
boolean flags alongside the payload:
partial—truewhen cost-basis enrichment did not finish within the request’s time budget.still_indexing— mirrorspartial.
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}Export truncation
Section titled “Export truncation”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-Truncated—trueif the upstream provider hit its page cap and the history is incomplete, otherwisefalse.
Both headers are exposed via CORS. The JSON export body additionally includes
count and truncated fields:
{ "transactions": [ /* ... */ ], "count": 5000, "truncated": true}Timeouts
Section titled “Timeouts”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.
