How the SDK thinks
Five decisions explain almost every API in this SDK. Knowing them means you can usually guess how something works instead of looking it up.
1. Everything hangs off a context
Section titled “1. Everything hangs off a context”A context holds the cache, the RPC binding, and the cluster’s known addresses. Creating one is synchronous and does no network work.
const sage = createSageClient({ cluster: 'zink-ptr', rpc });There is no global init(). Two contexts are genuinely independent — separate
caches, separate endpoints — which is what makes it safe to run several in one
process.
If you prefer functions to objects, createSageContext gives you the context
alone, and every domain function takes it as its first argument. The convenience
client is a thin wrapper over exactly those functions.
2. Reads return loaded data
Section titled “2. Reads return loaded data”There are no two-phase handles. When a read resolves, the data is there.
const character = await sage.characters.forProfile(profileAddress);character.name; // already loadedThe tradeoff is deliberate: anything that costs a network round trip is a
method you call, not a property you read. character.fleets.all() is a
separate read, so it is a separate await. If it looks like a property, it is
already in memory.
3. Nothing is trusted until it is validated
Section titled “3. Nothing is trusted until it is validated”Every account read is checked before decoding: program owner, discriminator, data shape, minimum length. Data that fails produces a typed error and never enters the cache.
This matters more than it sounds. An RPC can return anything — a wrong account, a truncated response, an account from a different program. Without validation those become silently wrong values deep in your application. Here they become an error at the read.
4. The cache is keyed by identity, not by call
Section titled “4. The cache is keyed by identity, not by call”Two reads of the same account through the same context return the same immutable snapshot until it expires. Not an equal copy — the same object.
The key is cluster, program, account type, and address. Reading a fleet through
character.fleets.all() and reading it directly hit the same cache entry.
Every snapshot carries provenance: where it came from and when. So “is this fresh?” is always answerable, and the interactive examples will later let you watch caching happen.
5. You pay for what you import
Section titled “5. You pay for what you import”The package has one entry point per capability:
import { getFleet } from '@aephia/sage/fleets';import { getStarSystem } from '@aephia/sage/world';Importing fleets does not pull in markets or crafting. Bundle budgets for
every entry are enforced in CI, so this stays true.
The root entry (@aephia/sage) is the convenience client, which composes the
common capabilities. It is the largest import and the easiest to start with.
Reach for capability entries when bundle size matters.
What this adds up to
Section titled “What this adds up to”Reads are explicit, results are trustworthy, and staleness is visible. The cost
is a little more ceremony than an SDK that hides its I/O — an extra await
where another library might give you a magic property.
That is a deliberate trade. The alternative hides network calls behind property access, which is exactly how a dashboard ends up making four hundred RPC requests a second without anyone noticing.
Where to go next
Section titled “Where to go next”- The map — how the entities relate to each other
- Reference — every public entry, generated from source
- For AI assistants — machine-readable docs, and the legacy-package trap