Rate limits

v1 status: rate limiting is being implemented. The 30 RPS per-credential limit described below is the design target — until the middleware ships, requests are not actively throttled. Build to the limit anyway: the moment we turn it on, partners exceeding 30 RPS will start receiving 429s. We'll announce a date in the changelog.

Two limits apply, in this order:

LayerLimitScope
Per-credential30 requests/secondEach gspk_live_* credential separately
Global upstream10 requests/secondAggregate across all GhostSwap partners (we absorb this)

Once enforcement is live, hitting the per-credential limit returns HTTP 429 with a Retry-After header. The global cap is mostly invisible to you — we queue and serialize internally.

Recognizing 429s

HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json
X-Request-Id: 7b3c1e9f-...
 
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limited",
    "message": "Per-credential rate limit exceeded",
    "retry_after_ms": 1000
  }
}

Backing off

Read Retry-After (in seconds) and sleep at least that long before retrying. Use the same Idempotency-Key if you're retrying a POST /v1/swaps — that way the retry is safe even if the original request succeeded server-side and we return the cached response.

async function withBackoff(fn, attempts = 5) {
  for (let i = 0; i < attempts; i++) {
    const res = await fn();
    if (res.status !== 429) return res;
    const retryAfter = Number(res.headers.get('Retry-After')) || 1;
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
  }
  throw new Error('Rate limit retries exhausted');
}

Avoiding rate limits

  • Cache GET /v1/currencies for ~10 minutes. The list rarely changes minute-to-minute.
  • Quote once per user attempt. Don't re-quote on every keystroke; debounce by ~500ms.
  • Poll GET /v1/swaps/:id every 30 seconds while a swap is non-terminal. There's no benefit to faster — our worker only updates the upstream every 30s.
  • Stop polling at terminal states. finished, failed, refunded, overdue, expired — never poll these again.

Need higher limits?

Email support@ghostswap.io with the credential id and your expected sustained RPS. We adjust the per-credential bucket on a case-by-case basis.

How GhostSwap absorbs the global cap

Our liquidity layer enforces a 10 RPS aggregate cap shared across all GhostSwap partners. We handle this for you:

  • Read endpoints (currencies, pairs, address validation, quotes, status) are queued internally so partners don't see those 429s.
  • Writes (swap creation) are serialized to fit under the cap.

If we ever need to surface a global 429 to you, the error type is upstream_error with code upstream_rate_limited — distinct from your per-credential rate_limited.