7.2 KiB
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 a fixed
allowlist of typed safe reads: character assets (paged GET), character asset names
and locations (bounded read-only POST), character wallet balance (unpaged GET), and
global market prices (unpaged GET). Callers cannot provide an arbitrary ESI
URL, host, method, or POST body.
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 bounded-read snapshot with
consistency: "bounded-non-atomic". 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.
Only page one is conditionally revalidated: revalidating every page would multiply
remote calls up to the bounded page cap and still could not make ESI's independently
served pages atomic. The explicit bounded-non-atomic coverage state prevents this
adapter from claiming transactional consistency. 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 a structured authorization
context: application user/principal, character, sorted scopes, grantRevision,
authorizationEpoch, and committed credentialVersion. Production callers pass
the current counters:
{ userId, principal, characterId, scopes, grantRevision, authorizationEpoch, credentialVersion }
Tokens are never used as cache keys or emitted in metrics. Private reads reject
legacy string keys and require all structured fields, including the matching
character, required scope, grantRevision, authorizationEpoch, and
credentialVersion. Changing any authority counter therefore cannot reuse a prior
private body or snapshot.
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 for both GET and declared
read-only POST operations. Canonical helper bodies are sorted decimal int64 JSON
literals, so ordering variants share one cache identity; operation/method/body
cannot collide. Helper requests accept 1..1000 unique positive signed-int64 IDs
and reject unsafe/duplicate/oversize input without Number coercion. Each operation
also enforces a response Content-Length/stream byte cap and result-row cap before
retaining a cache entry; asset pages and aggregate snapshots have separate bounds
(1,000 pages, 5,000 rows/page, 50,000 rows/snapshot). 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. Query a private cooldown with
client.getCooldown(authorizationKey, characterId) or the public limiter with
client.getPublicCooldown(). A server delay that exceeds the remaining request
budget returns a typed rate-limit error with retryAt, retryAfterMs, and
scheduler-readable cooldown rather than retrying early. Response cancellation is
abort-first and non-blocking, so cleanup cannot extend a deadline. Process-local
retention is bounded by weighted response bytes and aggregate snapshot rows;
client.getCacheStats() exposes current usage and both budgets. Eviction is
oldest-insertion deterministic. 401 and 403 are never blindly retried. OAuth exchange/refresh remain in EveSso and never use
these safe-read retries.
Asset names and locations
EsiClient.getAssetNames and getAssetLocations call fixed character helper
routes with the asset scope. Their outbound body is canonicalized and serialized
as numeric JSON literals (including IDs above 2^53). Typed observations include
requested IDs, returned values, explicit missing IDs, complete, and
retrieval/expiry provenance. A missing helper result is never silently represented
as complete enrichment. Duplicate or unexpected IDs, malformed names, non-finite
coordinates, and malformed arrays fail closed. Deterministic transport fixtures live
under tests/fixtures/esi/ so int64 names, finite positions, signed balances,
and versioned route/scope metadata are parsed from raw JSON rather than rounded
JavaScript literals. Asset helper metadata cites pinned OpenAPI evidence; wallet
metadata cites the current official OpenAPI retrieval and explicitly records that
it is not an immutable historical capture.
Wallet balance
EsiClient.getWalletBalance reads the unpaged character wallet route with the
wallet scope. The adapter returns only a transport-level observation with signed
integer cents in amount (via T04's half-away-from-zero decimalIskToCents
normalization) and retrievedAt / expiresAt; sub-cent source values are rounded
by that approved policy, not rejected. Persistence, scheduling, and UI/service
orchestration belong to later work. Negative and very large balances remain
lossless, while malformed/non-finite numeric bodies are rejected. The tracked
fixture metadata records the official OpenAPI URL, operation ID, 120-second
TTL, char-wallet limiter, response SHA-256
9a9a0f4af77216300e960cfd2576c9baac085a8170022e40c928c28622d80fc6, and the
2026-09-19T19:30:12Z retrieval. This is a current retrieval using the pinned
compatibility date, not an immutable historical capture from 2026-08-18. Official
references: ESI OpenAPI,
versioning,
caching,
and rate limiting.
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.