Errors & rate limits
All API errors use a consistent JSON envelope:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message."
}
}Unexpected server errors may use a legacy Fastify shape without error.code. Client code should handle non-2xx responses defensively.
Stack traces are never returned to clients.
Rate limiting
When Upstash Redis is configured, public asset and collection routes are rate limited per client IP using fixed-window counters in Redis.
Defaults (may change)
| Variable | Default | Meaning |
|---|---|---|
RATE_LIMIT_WINDOW_SECONDS | 60 | Window length in seconds |
RATE_LIMIT_MAX | 120 | Global requests per window per IP |
RATE_LIMIT_ROUTE_MAX | 60 | Additional cap per expensive route template |
Production asset GET /v1/assets/:mint exposes X-RateLimit-Limit: 60 from the **per-route** counter (the tighter of global + route limits is enforced).
Tiers
**Expensive** (global + per-route counters):
GET /v1/assets/:mint(+/health,/inspection,/media)POST /v1/assets/:mint/media/recoverGET /v1/collections/:address(+/assets,/health,/market)POST /v1/collections/:address/index
**Lightweight** (global counter only):
GET /v1/assets/:mint/snapshot,/snapshotsGET /v1/collections/:address/indexGET /v1/collection-index-jobs/:jobId
Exempt:
GET /health
**Not rate limited** (no matching rule):
- Monitor, event, and webhook routes
Response headers
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests in the current window (merged limit) |
X-RateLimit-Remaining | Remaining requests |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds to wait (429 only) |
429 envelope
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please retry after the window resets."
}
}Fail-open
If Redis errors during a rate limit check, the request is **allowed** and a warning is logged (RATE_LIMIT_CHECK_FAILED). Rate limiting is disabled entirely when Redis is not configured.
Client IP identity
Rate limit keys hash the resolved client IP. X-Real-IP is honored **only** when the socket peer is in TRUST_PROXY_CIDRS (includes 100.0.0.0/8 on Railway). Untrusted peers ignore spoofed X-Real-IP. **X-Forwarded-For is not used** for rate-limit identity.
Error catalog
| HTTP | Code | Meaning | Developer action |
|---|---|---|---|
| 400 | INVALID_ASSET_ID | Mint is not a valid Solana public key | Fix the address parameter |
| 400 | INVALID_COLLECTION_ADDRESS | Collection address invalid | Fix the address parameter |
| 400 | INVALID_QUERY_PARAMETER | Bad query value | Fix query string |
| 400 | INVALID_MONITOR_TARGET | Monitor target invalid | Provide valid mint/address |
| 400 | INVALID_MONITOR_INTERVAL | Interval out of range | Use allowed interval |
| 400 | INVALID_EVENT_TYPE | Unknown event type for target | Use enum values from monitoring docs |
| 400 | INVALID_WEBHOOK_URL | Webhook URL blocked or malformed | Use HTTPS URL passing SSRF checks |
| 401 | MONITORING_AUTH_REQUIRED | Missing/invalid Bearer token | Send Authorization: Bearer <private-beta-token> |
| 403 | MONITORING_WRITES_FORBIDDEN | Writes disabled on deployment | Use read APIs or request beta access |
| 404 | ASSET_NOT_FOUND | Asset not found upstream | Verify mint exists |
| 404 | SNAPSHOT_NOT_FOUND | No persisted snapshot | Wait for persistence or use live resolve |
| 404 | COLLECTION_NOT_FOUND | Collection not registered | POST .../index first |
| 404 | COLLECTION_NOT_INDEXED | Index not started | Start indexing |
| 404 | INDEX_JOB_NOT_FOUND | Job ID unknown | Verify job ID |
| 404 | MONITOR_NOT_FOUND | Monitor or event not found | Verify ID |
| 404 | WEBHOOK_NOT_FOUND | Webhook not found | Verify ID |
| 409 | INDEX_ALREADY_RUNNING | Active job + refresh=true | Poll existing job |
| 429 | RATE_LIMITED | Too many requests | Back off using Retry-After |
| 429 | MONITOR_LIMIT_REACHED | Max active monitors | Delete or pause monitors |
| 502 | UPSTREAM_ERROR | Provider failure | Retry with backoff |
| 502 | UPSTREAM_INVALID_RESPONSE | Provider returned invalid data | Retry later |
| 502 | WEBHOOK_DELIVERY_FAILED | Test delivery failed | Fix receiver endpoint |
| 503 | SERVICE_NOT_CONFIGURED | Resolver not configured | Contact operator |
| 503 | PERSISTENCE_UNAVAILABLE | Database not configured | Enable persistence or use live routes |
| 503 | MONITORING_WRITES_NOT_CONFIGURED | Writes enabled but no admin token | Server configuration issue |
| 504 | UPSTREAM_TIMEOUT | Provider timeout | Retry with backoff |
CORS
When CORS_ALLOWED_ORIGINS is set, browser requests must originate from an allowed origin. Exposed headers include rate limit and cache headers listed above.