# ESI operations The server ESI adapter uses the fixed trusted base `https://esi.evetech.net` and pins `X-Compatibility-Date: 2026-08-18` (`ESI_COMPATIBILITY_DATE`). It exposes only the character asset GET and global market-price GET needed by the application; callers cannot provide an arbitrary ESI URL. ## Asset synchronization `EsiClient.getAssets(characterId, accessToken, authorizationKey)` fetches page one, reads `X-Pages`, fetches all remaining pages, de-duplicates by `item_id` while checking for duplicates, and revalidates page one after the other pages. A changed page count, duplicate ID, malformed page, failed request, or changed first page fails the operation. The caller must not persist a returned partial result: the method returns only a complete `{ assets, syncedAt, expiresAt }` snapshot. Asset IDs are retained as decimal strings, and the response is parsed with `lossless-json` so an int64 cannot be rounded by JavaScript. This is a stability check, not a transaction. ESI's paginated endpoint does not promise a true multi-page snapshot while its cache expires between page requests. A caller should stage pages and replace the persisted Convex snapshot only after `getAssets` resolves. Convex persistence is intentionally outside this adapter. The cache is private to an `EsiClient` and keyed by user/authorization context, character, and scopes. `authorizationKey` should therefore identify the linked application user and include the character/scopes when available: ```ts { userId, characterId, scopes } ``` A string key is supported for callers that have no scope object, but is treated as an isolated user key. Tokens are never used as cache keys or emitted in metrics. Responses retain their raw text, ETag, Last-Modified, and cache metadata. Fresh responses use the server TTL; stale responses send `If-None-Match` and `If-Modified-Since`, and a 304 reuses the cached body. `Cache-Control`, `Expires`, `Date`, and `Age` are honored. `no-store` responses retain neither body nor validators. Requests have one deadline covering response-body reads and bounded retries for transient network/5xx/420/429 responses. `Retry-After` (seconds or HTTP date) and the legacy ESI error-limit reset are respected. Exhausted new limiter buckets are cooled down per authorization context and group; the legacy error limiter is cooled down globally. A server delay that exceeds the remaining request budget returns a typed rate-limit error rather than retrying early. 401 and 403 are never blindly retried. ## Market estimates `EsiMarketEstimates` reads public `GET /markets/prices`. It uses only `average_price`; a missing average price is omitted and `adjusted_price` is never a fallback. The returned `PriceSnapshot.source` includes the pinned compatibility date, `asOf` is the retrieval time (ESI supplies no source timestamp), and prices are integer ISK cents represented as validated decimal strings. The route is a cached global reference estimate, not a regional or structure order book. Metrics supplied through `EsiClient`'s `observe` callback include endpoint, status, retry/cache state, and redacted rate-limit headers. They do not include access tokens, authorization keys, or response bodies.