Arete documentation
TypeScript SDK
For the complete documentation index optimized for AI agents, see llms.txt or llms-full.txt. A markdown version of this page is available by appending.mdto the URL or sendingAccept: text/markdown.
For AI agents: the documentation index is at llms.txt (full corpus: llms-full.txt). A markdown source for this page is /sdks/typescript.md.
The @usearete/sdk package is the framework-agnostic client for generated
Program SDKs, live views, generic Solana reads, and transaction operations. It
uses AsyncIterable-based streaming and works in Node.js, browsers, Deno, and
Bun.
This is a reference page. If your agent writes the code, this is what it works from, and it is where to look when you want to understand or review what the agent wrote.
Protocol clients
Section titled “Protocol clients”Install with a4 install protocol <package> --ts and read the generated README
for its import. Use createProtocolClient on its own, or register a definition
under createSession({ protocols: { alias: definition } }, options) to access
session.protocols.alias with its connected stack and program dependencies.
Protocol clients expose typed step preparation, wallet actions, instances, participants, metadata, event streams, execution outcomes, and receipts. They use the protocol API independently of the stack’s WebSocket connection.
Integration guide · Prepare, sign, and submit · Protocol API reference
Installation
Section titled “Installation”npm install @usearete/sdkNo peer dependencies. Works anywhere JavaScript runs.
Quick Start
Section titled “Quick Start”import { createSession } from "@usearete/sdk";import { MY_STACK } from "./generated/my-stack";
const session = await createSession({ stacks: { app: MY_STACK },});
// Stream entities with full type safetyfor await (const position of session.stacks.app.views.Position.list.use()) { console.log(position);}MY_STACK and Position are placeholders. Install a subscribe-ready stack and
use its generated import and view names.
Cartridges and the Console
Section titled “Cartridges and the Console”Think of a generated stack as a cartridge: it packages a domain’s typed data and operations. Think of the Arete session as the console that connects those cartridges to shared chain reads and execution. These are mental models only—the API keeps the existing generated stack objects and types.
A connected stack at session.stacks.<name> has these stable namespaces:
| Namespace | Purpose |
|---|---|
views | Typed live and point-in-time entity views |
queries | Stack-level HTTP queries |
programs | Programs packaged with the stack |
addresses | Domain address derivations, when defined |
constants | Domain constants and enums, when defined |
defaults | Reusable domain defaults, when defined |
math | Pure domain calculations, when defined |
read | Connected domain reads, when defined |
flows | Stack-level multi-transaction operations, when defined |
Programs at session.stacks.<name>.programs.<program> and session.programs.<name> have these stable namespaces: programId, schemas, pdas, addresses, accounts, queries, raw, instructions, transactions, flows, constants, defaults, and math.
Use session.chain for generic Solana reads and session.execute(prepared) for
any prepared instruction, transaction, or flow.
For transaction inspection and execution, connected sessions automatically use the authenticated Arete transaction transport. See Transactions for auto, direct, and custom transport modes, scopes, and safe failure handling.
Application Operations vs. Raw IDL
Section titled “Application Operations vs. Raw IDL”Prefer generated application operations when they are available. They accept normal camelCase objects, derive routine addresses, normalize values such as UI token amounts, and describe their cardinality through the namespace:
const prepared = await session.stacks.presale.programs.presale.flows.permissionlessDeposit.prepare({ presale, owner, quoteMint, payerQuoteToken, tokenProgram, maxAmount: { ui: "10.5" },});
await session.execute(prepared);Use program.raw as an escape hatch when you need the exact generated IDL shape. Raw names and nested fields are not normalized; snake_case remains snake_case. Raw .build(...) returns one instruction for manual composition:
const instruction = session.stacks.presale.programs.presale.raw.deposit.build({ max_amount: 10_500_000n, remaining_account_info: { slices: [] }, presale, quote_mint: quoteMint, escrow, payer_quote_token: payerQuoteToken, token_program: tokenProgram, program: session.stacks.presale.programs.presale.programId,});
await session.transaction([instruction]);Connection
Section titled “Connection”Create a Session
Section titled “Create a Session”Each generated stack definition includes its endpoints, so inserting it into a session is simple:
import { createSession } from "@usearete/sdk";import { MY_STACK } from "./generated/my-stack";
const session = await createSession({ stacks: { app: MY_STACK },});
// Now you have fully typed viewsconst positions = await session.stacks.app.views.Position.list.get({ take: 20 });
// Generic chain reads belong to the session consoleconst lamports = await session.chain.lamports(owner);Multiple Stacks and Programs
Section titled “Multiple Stacks and Programs”Name each inserted stack or standalone program. Stack names become properties under session.stacks; programs packaged by those stacks and explicitly attached programs are available under session.programs:
const session = await createSession({ stacks: { markets: MARKETS_STACK, positions: POSITIONS_STACK, }, programs: { splToken: SPL_TOKEN_PROGRAM, },});
session.programs.market;session.programs.splToken;Promotion reuses the stack’s connected program object and endpoint. session.stacks.<name>.programs.<program> always stays available.
Programs are matched by identity, not by name. A program SDK’s identity is its packageReleaseHash: the program package release a registry-installed SDK was generated from. Local builds have none. When two programs claim one key:
- Both have a
packageReleaseHash. Equal hashes, or the same object, mean the same program. A standalone program that a stack also provides is then served by that stack’s connected instance, sosession.programs.ore === session.stacks.ore.programs.oreand there is one client, not two. Configure it through the stack member’s options. The exception is a standalone program whoseprogramReads.<key>descriptor in the session definition differs from the one the stack reads through. It keeps its own read configuration and is handled like the unproven case below, with one warning. Different hashes throw anAreteErrorwith codePROGRAM_KEY_CONFLICTbefore anything connects. - At least one has none, with the same
programSpecHash. The programs describe the same on-chain program but cannot be proven to be the same SDK, for example a local stack and a local standalone build of its program. The program you attached explicitly takes the key, and the SDK logs one warning saying so. A standalone session program takessession.programs.<key>whilesession.stacks.<name>.programs.<key>keeps the stack’s program. When two stacks bundle such copies, the first stack’s program takessession.programs.<key>. - Anything else is a conflict. A different or missing
programSpecHashthrowsPROGRAM_KEY_CONFLICT. The message names the stack and key: usesession.stacks.<name>.programs.<key>, or attach the standalone program under a different key. When two stacks bundle different programs under one key, both stay reachable throughsession.stacks, and readingsession.programs.<key>throwsPROGRAM_KEY_CONFLICTwith the stack-scoped paths to use instead.
The same rule applies to programs attached to one stack with withPrograms(stack, programs), Arete.connect(stack, { programs }), a session member’s programs, and React’s useArete(stack, { programs }). There the attached program replaces the stack’s under that key for that client, and the stack definition itself is unchanged.
extendProgram, extendPrograms, and withProgramRead drop packageReleaseHash, because a program changed outside its generated SDK is no longer provably that SDK. A program you extend yourself is therefore never mistaken for the stack’s copy: attached under the stack’s key, it takes that key with a warning instead of being silently replaced by the stack’s program. Generated SDK entries stamp their identity last with withProgramIdentity(program, { packageReleaseHash }), after applying the package’s own extension.
Direct Single-Stack Connection
Section titled “Direct Single-Stack Connection”Arete.connect(...) remains available for direct single-stack clients and advanced per-connection configuration:
import { Arete } from "@usearete/sdk";
const client = await Arete.connect(MY_STACK, { url: "wss://custom.endpoint.com",});Connection Options
Section titled “Connection Options”const client = await Arete.connect(MY_STACK, { maxEntriesPerView: 5000, // Limit entries per view (default: 10000) autoConnect: true, // Start the initial connection autoReconnect: true, // Recover after an established socket is lost});| Option | Type | Default | Description |
|---|---|---|---|
url | string | stack.url | Override the stack’s default URL |
maxEntriesPerView | number | null | 10000 | Max entries per view before LRU eviction |
autoConnect | boolean | true | Start the initial WebSocket connection |
autoReconnect | boolean | true | Recover after an established WebSocket connection is lost |
validateFrames | boolean | true | Log generated-schema frame rejections |
onFrameValidationError | function | — | Observe structured generated-schema frame rejections |
apiUrl | string | https://api.arete.run | Arete API origin for client.platform |
platform | PlatformHttpClient | built from auth | Arete API client to use; see Arete API client |
environment | 'production' | { kind: 'dev', url?, env? } | ARETE_ENVIRONMENT, else 'production' | Production or the project’s dev environment; see Dev environment |
autoConnect and autoReconnect control separate lifecycle phases. Set autoConnect: false to create a disconnected client that the application connects later. Set autoReconnect: false when the application will handle a later connection loss itself.
Dev environment
Section titled “Dev environment”The same installed SDKs run against production or against your project’s dev environment (a fork of mainnet). Nothing is regenerated; one switch decides, once per client:
- Unset, or
ARETE_ENVIRONMENT=production: production, exactly as generated. ARETE_ENVIRONMENT=dev(servers and scripts) orenvironment: { kind: 'dev' }: the dev environment. The target is the edgea4 dev upserves (ARETE_DEV_URL, or found from the runninga4 dev up), or an environment reached directly with a secret key (ARETE_DEV_ENV).a4 dev run -- <command>sets these for you.- Browsers read no variables: pass the option from your framework’s public variables, for example
environment: import.meta.env.VITE_ARETE_ENVIRONMENT === 'dev' ? { kind: 'dev', url: import.meta.env.VITE_ARETE_DEV_URL } : 'production'. Through the edge the page holds no Arete key.
const client = await Arete.connect(MY_STACK, { environment: { kind: 'dev' } });client.environment; // 'dev'Chain reads, transactions, program account reads, the stack stream and queries, and protocol clients all follow the switch. A surface the environment can’t serve fails with a DevEnvironmentError whose code (devenv-not-configured, devenv-edge-unreachable, devenv-stack-not-loaded, …) and fix say what to do; the SDK never falls back to production. In dev mode transactions are signed by the wallet and sent through the client’s transaction transport, so the wallet adapter must be sign-only (the Arete kit and web3.js adapters are, unless you give them their own transport). url and httpUrl can’t be combined with dev mode. createSession and createProtocolClient take the same environment option.
Connection State
Section titled “Connection State”// Check current stateconsole.log(session.stacks.app.connectionState);// 'connected' | 'connecting' | 'disconnected' | 'reconnecting' | 'error'
// Listen for changesconst unsubscribe = session.stacks.app.onConnectionStateChange((state) => { console.log("Connection state:", state);});
// Later: stop listeningunsubscribe();Disconnect
Section titled “Disconnect”session.close();Authentication
Section titled “Authentication”Choose the credential by where the code runs and who it acts for:
auth | Runs in | Acts for |
|---|---|---|
publishableKey (a4_pk_...) | A browser | A visitor to your app, bound to its origins |
secretKey (a4_ak_... or a4_sk_...) | A server or script | Your Arete account |
token, getToken or tokenEndpoint | Anywhere | The target your backend minted the session token for |
token, getToken or tokenEndpoint with tokenKind: 'api' | A browser or server | An Arete user signed in to your app |
Connect to Live Views covers keys and
the a4 login fallback. This section covers the API credential and the Arete
API client built from it.
Where account credentials go
Section titled “Where account credentials go”An account credential is your own Arete credential: a secret or agent key
(secretKey, ARETE_API_KEY or the a4 login) or any token, getToken or
tokenEndpoint with tokenKind: 'api'. The SDK sends it only to trusted Arete
API origins, however it was supplied:
https://api.arete.run, always.- The origin of
ARETE_API_URL, outside browsers. - The origin of an
apiUrlyou set in code (createSession,Arete.connect,createPlatformHttpClientor a protocol client), for a local, development or self-hosted API. This only covers credentials you pass inauth.
Credentials the SDK finds for you are narrower. An ARETE_API_KEY key goes to
https://api.arete.run and the ARETE_API_URL origin. A key from the a4
login goes only to https://api.arete.run, the API it was stored for.
A session endpoint named by a stack, binding or generated descriptor is not
trusted just because it is named, and neither is a sessionEndpoint override.
Setting apiUrl doesn’t change that: the session endpoint must itself be on a
trusted origin. When it isn’t, the SDK sends nothing. The call rejects with an
AreteError whose code is UNTRUSTED_CREDENTIAL_DESTINATION and whose
details name the destination origin, the credential and the
trustedOrigins. To allow an API you run, set apiUrl to its origin (or
ARETE_API_URL), and pass the key in auth.secretKey if it came from the a4
login. A WebSocket that hits this error stops reconnecting.
Publishable keys and Arete session tokens (tokenKind: 'session', the default)
are not account credentials. They go to the endpoints they were made for, as
before.
Sign in with an Arete API credential
Section titled “Sign in with an Arete API credential”By default (tokenKind: 'session'), token, getToken and tokenEndpoint
hand out Arete session tokens, and the SDK sends them to stack, gateway,
Program Read and protocol endpoints as they are. When your app’s users are
signed in to Arete, hand the SDK the user’s own Arete API credential instead
and set tokenKind: 'api':
import { createSession } from "@usearete/sdk";
// Your backend returns the signed-in user's Arete API credential.const session = await createSession( { stacks: { app: MY_STACK } }, { auth: { tokenKind: "api", tokenEndpoint: "/api/arete-credential", tokenEndpointCredentials: "include", }, },);A getToken provider or a static token works the same way. With
tokenKind: 'api':
- Where the credential goes. The SDK sends it only to Arete API routes and
to session endpoints. A stack, gateway or Program Read endpoint never
receives it, and a session endpoint is used only on a trusted origin (see
Where account credentials go). Without a
token,getTokenortokenEndpoint, the credential falls back toARETE_API_KEY, then to thea4login. - Binding-scoped sessions. For each hosted transport (the Solana gateway’s chain reads and transactions, Program Read, protocol clients and hosted stack WebSockets) the SDK mints a session at that binding’s session endpoint, with the credential as the bearer, and sends only that session to the transport.
- Caching. Minted sessions are cached per target and scope, and renewed before they expire.
- Refusals. When a session endpoint refuses the credential with a 401, the
SDK drops it, fetches the credential again, once, with
forceRefresh: true, and retries. Other requests wait for the new credential rather than send the refused one, and a 401 for a credential that was already replaced does not fetch it again. A statictokencannot be refetched, so its 401 is returned as an error.
getToken is called with no target, as getToken({ scopes: [], forceRefresh }).
tokenEndpoint is POSTed {"scopes": [], "forceRefresh": false} with your
tokenEndpointHeaders and tokenEndpointCredentials. Either can answer with
the credential as a string or as { "token": "...", "expiresAt": 1767225600 }
(seconds since the epoch). Without expiresAt the SDK reads a JWT’s exp; a
credential with neither is kept until it is refused.
One credential per session
Section titled “One credential per session”A session builds one credential source from its auth and shares it with every
member, transport and its Arete API client. A page that opens several stacks,
Program Reads and gateway transports therefore asks your getToken or
tokenEndpoint once per credential expiry, and concurrent requests wait on
the same fetch. A client from Arete.connect shares one source across its own
connections in the same way. A session member configured with its own auth
gets its own source and its own Arete API client.
forceRefresh
Section titled “forceRefresh”Every getToken request carries forceRefresh. It is true when the SDK
needs a new token: the last one was refused, has expired, or did not cover the
scopes now needed. A provider with its own cache must bypass that cache when
the flag is set, because returning the cached token again gets the same
refusal:
import type { AuthTokenRequest, AuthTokenResult } from "@usearete/sdk";
let cached: AuthTokenResult | undefined;
async function getToken(request?: AuthTokenRequest): Promise<AuthTokenResult> { const fresh = cached?.expiresAt !== undefined && cached.expiresAt * 1000 > Date.now() + 30_000; if (cached && fresh && !request?.forceRefresh) return cached; cached = await signIn.fetchAreteCredential(); // your app's sign-in return cached;}
const session = await createSession( { stacks: { app: MY_STACK } }, { auth: { tokenKind: "api", getToken } },);With tokenKind: 'api', tokenEndpoint request bodies carry forceRefresh
too, so a token endpoint that caches can do the same.
Arete API client
Section titled “Arete API client”A client or session whose auth carries an API credential (a secretKey, or
tokenKind: 'api') builds an authenticated Arete API client and exposes it as
session.platform or client.platform. With a publishable key, Arete session
tokens or no auth, platform is undefined. When the credential may not go
to apiUrl (an ARETE_API_KEY or a4 login key with an apiUrl on another
origin), platform still exists. Each request then rejects with
UNTRUSTED_CREDENTIAL_DESTINATION before anything is sent, so the error shows
up where you make the call. createSession options and
Arete.connect options take two related settings:
apiUrl: the Arete API origin, with or without/api. Defaults tohttps://api.arete.run. Setting it trusts that origin for the credential inauth.platform: an Arete API client to use instead of the one built fromauth.
To make Arete API calls without a session, create a client directly:
import { AreteError, createPlatformHttpClient } from "@usearete/sdk";
const platform = createPlatformHttpClient({ auth: { secretKey: process.env.ARETE_API_KEY },});
try { // A hypothetical route, for illustration only. const { body } = await platform.request<{ items: string[] }>({ path: "/example/items", }); console.log(body.items);} catch (error) { if (error instanceof AreteError) console.error(error.code, error.apiProblem()); throw error;}createPlatformHttpClient(options) takes:
apiUrl: as above.auth:secretKey,token,getTokenortokenEndpoint, each read as an API credential. Outside browsers an unsetauthfalls back toARETE_API_KEY, then to thea4login.fetch: a custom fetch implementation.
It refuses a publishable key with INVALID_CONFIG, because a publishable key
identifies a visitor to an app, not an account. It also refuses
tokenKind: 'session' with a token strategy, since Arete API routes don’t take
stack or gateway sessions, and throws AUTH_REQUIRED when there is no
credential at all. When the credential may not go to apiUrl, it throws
UNTRUSTED_CREDENTIAL_DESTINATION. It never mints a session.
platform.request({ path, method, body, signal }) sends path under
platform.apiUrl (which ends in /api), with body serialized as JSON, and
resolves to { status, body }. A non-2xx answer rejects with an AreteError
whose details carry status, wireErrorCode, the parsed body and
apiProblem. After a 401, a client whose credential comes from getToken or
tokenEndpoint fetches a fresh one and retries once.
Arete API calls in SDK extensions
Section titled “Arete API calls in SDK extensions”An SDK extension’s program operations and reads receive a
ProgramOperationContext with chain, wallet, program and platform.
context.platform is the client’s Arete API client, so an extension can call
the Arete API without handling credentials or fetch itself. Stack extensions
receive the connected client and read client.platform.
platform exists only when the app’s auth carries an API credential: a
secret key on a server, or tokenKind: 'api' for a signed-in Arete user. With
a publishable key it is undefined. A publishable key identifies a visitor to
an app, never the account, so it never gets an Arete API client. Public data
reaches the browser through the program’s own session transports instead:
Program Read, the Solana gateway and stack views.
Check for it and fail with a message that says what the app must configure:
import { AreteError, defineProgramExtensions } from "@usearete/sdk/program";import type { VAULT } from "./vault-core.js";
interface Watchlist { addresses: string[];}
export default defineProgramExtensions<typeof VAULT>()({ createRead(context) { return { async watchedVaults() { const platform = context.platform; if (!platform) { throw new AreteError( "read.watchedVaults calls the Arete API: authenticate with a " + "secret key on a server, or auth.tokenKind 'api' for a signed-in " + "Arete user. A publishable key cannot call it.", "PLATFORM_CLIENT_REQUIRED", ); } // A hypothetical route, for illustration only. const { body } = await platform.request<Watchlist>({ path: "/example/watchlist", }); return context.program.accounts.Vault.fetchMany(body.addresses); }, }; },});The app calls it like any other extension read:
const session = await createSession( { programs: { vault: VAULT_PROGRAM } }, { auth: { secretKey: process.env.ARETE_API_KEY } },);
const vaults = await session.programs.vault.read.watchedVaults();Extension code imports only types for this (PlatformHttpClient,
PlatformHttpRequest, PlatformHttpResponse and AuthConfig are exported
from @usearete/sdk/program), so it never bundles the client runtime. An
extension can also accept a PlatformHttpClient the app passes in, for code
that runs outside a connected client.
View Modes
Section titled “View Modes”Every view operates in one of two modes, which determines how you access data:
| Mode | Description | Key Required | Returns |
|---|---|---|---|
| State | Lookup individual entities by key | Yes | Single entity (T | null) |
| List | Access a collection of entities | No | Array of entities (T[]) |
Default Views
Section titled “Default Views”By default, each entity in a stack exposes two views:
| View Name | Mode | Description |
|---|---|---|
state | State | Lookup any entity by its key (e.g., address) |
list | List | Collection of all entities |
// State view — get a specific round with its generated keyconst round = await session.stacks.ore.views.OreRound.state.get({ roundId });
// List view — get all roundsconst rounds = await session.stacks.ore.views.OreRound.list.get();Custom Views
Section titled “Custom Views”Stacks can define additional views beyond the defaults. For example, the ORE stack includes a custom latest view that streams only recent rounds. Custom views are defined using the Rust DSL when building your stack — see Stack Definitions for details.
Custom views are accessed the same way as default views:
// Custom list view defined in the ORE stackfor await (const round of session.stacks.ore.views.OreRound.latest.use()) { console.log("Recent round:", round.id.roundId);}Each view type supports both streaming (real-time updates) and one-shot (point-in-time snapshot) access patterns.
Streaming Methods
Section titled “Streaming Methods”Streaming methods return AsyncIterable and continuously emit data as entities change. The connection stays open until you break out of the loop or abort.
| Method | Emits | Use Case |
|---|---|---|
.use() | T | Simplest — just the current entity state after each change |
.watch() | Update<T> | When you need to know the operation type and membership change |
.watchRich() | RichUpdate<T> | When you need before/after comparison |
.use() — Stream Merged Entities
Section titled “.use() — Stream Merged Entities”The simplest streaming method. Emits the full merged entity after each change:
// List view — no key requiredfor await (const round of session.stacks.ore.views.OreRound.latest.use()) { console.log("Round:", round.id.roundId); console.log("Motherlode:", round.state.motherlode);}
// State view — key requiredfor await (const round of session.stacks.ore.views.OreRound.state.use({ roundId })) { console.log("Round updated:", round.state.motherlode);}Signatures:
// State view; key type is generated from the entity primary keyuse(key: TKey, options?: WatchOptions): AsyncIterable<T>
// List viewuse(options?: WatchOptions): AsyncIterable<T>.watch() — Stream Canonical Updates
Section titled “.watch() — Stream Canonical Updates”Use when you need to know what operation occurred. These updates use the same canonical SDK-facing shape as .use() and .get(), including camelCase field names and bigint values for large integer families:
for await (const update of session.stacks.ore.views.OreRound.latest.watch()) { switch (update.type) { case "upsert": console.log("Created or replaced:", update.data); break; case "patch": console.log("Partial update:", update.data); break; case "remove": console.log("Left this query:", update.key); break; case "delete": console.log("Deleted:", update.key); break; }}Signatures:
// State viewwatch(key: TKey, options?: WatchOptions): AsyncIterable<Update<T>>
// List viewwatch(options?: WatchOptions): AsyncIterable<Update<T>>Update types:
type Update<T> = | { type: "upsert"; key: string; data: T } // Full entity create/replace | { type: "patch"; key: string; data: Partial<T> } // Partial update | { type: "remove"; key: string } // Left this query only | { type: "delete"; key: string }; // Deleted from the source view.watchRich() — Stream with Before/After Diffs
Section titled “.watchRich() — Stream with Before/After Diffs”Use when you need to compare the previous and new canonical state:
for await (const update of session.stacks.ore.views.OreRound.latest.watchRich()) { switch (update.type) { case "created": console.log("New entity:", update.data); break; case "updated": console.log(`Changed: ${update.before.state.motherlode} → ${update.after.state.motherlode}`); break; case "removed": console.log("Left this query:", update.lastKnown); break; case "deleted": console.log("Deleted:", update.lastKnown); break; }}Signatures:
// State viewwatchRich(key: TKey, options?: WatchOptions): AsyncIterable<RichUpdate<T>>
// List viewwatchRich(options?: WatchOptions): AsyncIterable<RichUpdate<T>>RichUpdate types:
type RichUpdate<T> = | { type: "created"; key: string; data: T } | { type: "updated"; key: string; before: T; after: T; patch?: unknown } | { type: "removed"; key: string; lastKnown?: T } | { type: "deleted"; key: string; lastKnown?: T };One-Shot Methods
Section titled “One-Shot Methods”One-shot methods return a point-in-time snapshot instead of a stream of updates. Use these when you need the current state but don’t need real-time streaming.
| Method | Returns | Behavior |
|---|---|---|
.get() | Promise<T> | Async — waits for the snapshot, then releases the subscription |
.getOne() | Promise<T | null> | Async — get() with take: 1, first item of a list view |
.getSync() | T | undefined | Sync — reads an already active subscription, undefined if none |
.get() — Async Snapshot
Section titled “.get() — Async Snapshot”Opens a subscription for the query (or reuses an equivalent one that is already active), waits for its initial snapshot, and releases it. A query that fails rejects with its error, and so does a read cut short by a failed or closed connection. If no snapshot arrives within timeoutMs (5000 by default), the promise rejects with InitialDataTimeoutError; pass timeoutMs: null to wait indefinitely.
// List view — returns all entitiesconst rounds = await session.stacks.ore.views.OreRound.latest.get();console.log(`Found ${rounds.length} rounds`);
// State view — returns single entity or nullconst round = await session.stacks.ore.views.OreRound.state.get({ roundId });if (round) { console.log("Round:", round.id.roundId);}Signatures:
// State view — returns single entity or null if not foundget(key: TKey, options?: GetOptions): Promise<T | null>
// List view — returns array of all entitiesget(options?: GetOptions): Promise<T[]>import { InitialDataTimeoutError } from "@usearete/sdk";
try { const rounds = await session.stacks.ore.views.OreRound.latest.get({ timeoutMs: 2_000 });} catch (error) { if (error instanceof InitialDataTimeoutError) { console.log(`No snapshot for ${error.view} within ${error.timeoutMs}ms`); }}.getOne() — First Item
Section titled “.getOne() — First Item”Reads the first item of a list view through a take: 1 query. Resolves null when the list is empty:
const latest = await session.stacks.ore.views.OreRound.latest.getOne();Signature:
getOne(options?: Omit<GetOptions, "take">): Promise<T | null>.getSync() — Synchronous Snapshot
Section titled “.getSync() — Synchronous Snapshot”Returns immediately with the data of an already active equivalent subscription (for example one held by a running .use() loop or a React hook). Returns undefined when no such subscription exists; it never opens one:
// List viewconst rounds = session.stacks.ore.views.OreRound.latest.getSync();if (rounds) { console.log(`Cached: ${rounds.length} rounds`);} else { console.log("Data not yet loaded");}
// State viewconst round = session.stacks.ore.views.OreRound.state.getSync({ roundId });Signatures:
// State view — returns entity, null (not found), or undefined (not loaded)getSync(key: TKey): T | null | undefined
// List view — returns array or undefined (not loaded)getSync(): T[] | undefinedStore Size Limits
Section titled “Store Size Limits”Each view maintains an in-memory store of entities. By default, stores are limited to 10,000 entries to prevent memory issues on long-running clients. When the limit is reached, oldest entries are evicted (LRU).
const ore = await Arete.connect(ORE_STREAM_STACK, { maxEntriesPerView: 5000, // Custom limit});To disable limiting (not recommended for production):
const ore = await Arete.connect(ORE_STREAM_STACK, { maxEntriesPerView: null, // Unlimited});Subscription Options
Section titled “Subscription Options”The streaming methods (.use(), .watch(), .watchRich()) accept options for pagination and validation:
interface WatchOptions { partition?: string; // Exact _partition match filters?: Record<string, unknown>; // Server-side dot-path equality filters take?: number; // Live query window size skip?: number; // Live query window offset withSnapshot?: boolean; // Include initial snapshot (default true) after?: string; // Incremental _seq cursor snapshotLimit?: number; // Initial snapshot row cap schema?: Schema<T>; // Client-side schema validation}Every options object defines exact protocol v2 query membership. Different windows and filters on one view can coexist without leaking entities, while equivalent normalized queries share one reference-counted wire subscription:
const rounds = session.stacks.ore.views.OreRound.latest;
const firstPage = rounds.watch({ take: 10 });const secondPage = rounds.watch({ take: 10, skip: 10 });After reconnect, a completed authoritative snapshot atomically replaces membership for its exact query. A query with after receives an incremental snapshot and merges without pruning.
Limit Results
Section titled “Limit Results”// Only receive the first 10 entitiesfor await (const round of session.stacks.ore.views.OreRound.latest.use({ take: 10 })) { console.log("Round:", round.id.roundId);}Pagination
Section titled “Pagination”// Skip first 20, take next 10for await (const round of session.stacks.ore.views.OreRound.latest.use({ skip: 20, take: 10 })) { console.log("Round:", round.id.roundId);}Server-Side Filtering
Section titled “Server-Side Filtering”Use exact-match dot-path filters for dynamic equality predicates:
for await (const order of session.stacks.market.views.Order.list.use({ filters: { "state.status": "open", "market.symbol": "SOL" }, take: 10,})) { console.log(order);}Filtering is case-sensitive and type-sensitive. Filters run before skip and take. Use a custom view when filtering, sorting, or aggregation needs to be declared as part of the stack rather than supplied by each client.
Filter keys are WebSocket wire-schema dot paths, so snake_case remains correct there. Generated TypeScript entity properties and generated state keys use camelCase.
See Filtering Feeds for details on all filtering options, or Stack Definitions for how to define custom views using the Rust DSL.
Stream Control
Section titled “Stream Control”Use standard AsyncIterable patterns to control streams client-side.
Stop on Condition
Section titled “Stop on Condition”for await (const update of session.stacks.ore.views.OreRound.latest.watch()) { if (update.type === "upsert") { const round = update.data; if (round.state.motherlode && round.state.motherlode > 1_000_000_000) { console.log("Found high-value round:", round.id.roundId); break; } }}Cancellable Streams
Section titled “Cancellable Streams”Use an AbortController to cancel from outside the loop:
const controller = new AbortController();setTimeout(() => controller.abort(), 30_000); // Cancel after 30s
try { for await (const update of session.stacks.ore.views.OreRound.latest.watch()) { if (controller.signal.aborted) break; console.log("Update:", update.data); }} catch (e) { if (!controller.signal.aborted) throw e;}Client-Side Filtering
Section titled “Client-Side Filtering”for await (const update of session.stacks.ore.views.OreRound.latest.watch()) { if (update.type !== "upsert") continue; if ((update.data.metrics.deployCount ?? 0) < 100) continue;
console.log("Active round:", update.data.id.roundId);}Schema Validation
Section titled “Schema Validation”Every stack ships with Zod schemas alongside its TypeScript interfaces. Use them to validate data at two levels:
Frame Validation
Section titled “Frame Validation”Generated schemas always normalize and validate frames before storage. Invalid data is dropped and logged by default. Observe structured failures without scraping console output:
const ore = await Arete.connect(ORE_STREAM_STACK, { onFrameValidationError(diagnostic) { console.error('Rejected Arete frame', diagnostic); },});Set validateFrames: false to suppress the default warning while keeping normalization, rejection, and onFrameValidationError active.
Patches Never Create Entities
Section titled “Patches Never Create Entities”A patch frame carries only the fields that changed. When it names a key the client holds no entity for (never received, or evicted by maxEntriesPerView), the SDK discards it rather than storing a partial object typed as complete: no store write, no query membership, no update. The entity appears when the server sends a full upsert for that key, and later patches merge into it as usual. Records from replayable append views carry an offset; they are events, not entity state, and are always applied.
Each discarded patch is reported to onFrameValidationError with reason: 'unknown-key' (schema rejections have reason: 'schema'). Only schema rejections are written to the console.
Query-Level Validation
Section titled “Query-Level Validation”Pass a schema to .use() to filter out entities that don’t match:
import { OreRoundCompletedSchema } from "./generated/ore-stack";
// Only emit rounds where every field is present. Rejected entities are skipped.for await (const round of session.stacks.ore.views.OreRound.latest.use({ schema: OreRoundCompletedSchema,})) { console.log(round.id.roundId, round.state.motherlode);}React view hooks additionally support onSchemaValidationError when an application needs diagnostics for caller-supplied schema rejections.
See Schema Validation for the full guide on generated schemas, custom schemas, and the “Completed” schema pattern.
Resolved Data
Section titled “Resolved Data”Arete can automatically enrich your entities with data that doesn’t live on-chain. For example, the ORE stack includes token metadata (name, symbol, decimals, logo) resolved server-side from the DAS API:
for await (const round of session.stacks.ore.views.OreRound.latest.use()) { // Token metadata is resolved automatically — no extra API calls console.log(round.oreMetadata?.name); // "Ore" console.log(round.oreMetadata?.symbol); // "ORE" console.log(round.oreMetadata?.decimals); // 11}Resolved data arrives as part of the entity alongside on-chain fields. Your client code reads it the same way — no distinction between on-chain and resolved data.
See Resolvers for how to add resolved data when building your own stack.
Amount Conversion
Section titled “Amount Conversion”toRawAmount(input, decimals) is convenient when invalid input should throw. Use safeToRawAmount(input, decimals) at user-input boundaries:
import { safeToRawAmount } from "@usearete/sdk";
const parsed = safeToRawAmount({ ui: amountText }, 9);if (!parsed.success) { console.error(parsed.error); return;}
console.log(parsed.data); // bigint in raw base unitssafeToRawAmount returns { success: true, data } | { success: false, error } instead of throwing for invalid amount input. It is also re-exported by @usearete/react.
Error Handling
Section titled “Error Handling”import { AreteError, createSession } from "@usearete/sdk";import { ORE_STREAM_STACK } from "./generated/ore-stack";
try { const session = await createSession({ stacks: { ore: ORE_STREAM_STACK } });} catch (error) { if (error instanceof AreteError) { console.error("Arete error:", error.message); console.error("Code:", error.code); } else { throw error; }}Testing
Section titled “Testing”@usearete/sdk/testing contains supported helpers for testing an application without a network, wallet, or chain. They depend only on @usearete/sdk and work under any test runner.
| Helper | Purpose |
|---|---|
createWebSocketHarness() | Scripted WebSocket server for a real client: pass harness.websocketFactory as auth.websocketFactory, then await subscriptions and answer them with frames |
frames | Builders for protocol v2 server frames (subscribed, snapshot, upsert, patch, remove, delete, error) |
createFrameHarness() | The client store engine without a socket: feed frames, read query snapshots |
createFakeTransactionTransport() | Recording transaction relay. calls shows whether anything was sent; options make it fail, stall, or report a status |
createWalletFixture() | Recording wallet adapter that holds no keys. responses scripts each send: a receipt, a failure outcome, an error, or 'stall' |
createTransactionOutcomeFixtures() | One outcome for every status and phase: confirmed, not-submitted, submitted-unknown, chain-failed |
createFetchStub(routes) | Routed, recording fetch for program reads, chain reads, and stack queries. bigint values are written as decimal strings |
import { Arete } from "@usearete/sdk";import { createWalletFixture, createTransactionOutcomeFixtures, createWebSocketHarness, frames,} from "@usearete/sdk/testing";
const ws = createWebSocketHarness();const wallet = createWalletFixture({ responses: [createTransactionOutcomeFixtures()["submitted-unknown:send"]],});const client = await Arete.connect(MY_STACK, { auth: { websocketFactory: ws.websocketFactory }, wallet,});
const updates = client.views.Position.list.watch()[Symbol.asyncIterator]();const first = updates.next();const subscription = await ws.waitForSubscription("Position/list");ws.serve(subscription, [{ key: "p1", data: { id: "p1", size: "10" } }]);await first; // the snapshot row, as an upsertws.send(frames.patch(subscription, "p1", { size: "12" }));
ws.restore();On runtimes without a global WebSocket (Node.js 20), the harness installs a stand-in while it is in use; restore() removes it. @usearete/react/testing re-exports these helpers and adds React-specific fixtures.
API Reference
Section titled “API Reference”EXTENSION_API_VERSION
Section titled “EXTENSION_API_VERSION”The version of the extension contract: defineStackExtensions, defineProgramExtensions, extendProgram, extendPrograms, extendStack, withProgramRead, and the instruction helpers generated code imports. It changes only on a breaking change to that contract, so generated SDKs and extensions keep working across SDK releases that export the same value. Tools can read it without loading the package from package.json as arete.extensionApi.
An SDK extension’s extensions.json can declare the value it was written against as extensionApi. a4 refuses to generate an SDK with an extension whose extensionApi differs from the installed @usearete/sdk’s, and a4 doctor reports the same mismatch for generated code (see SDK extension compatibility).
Generated stack and program objects keep their runtime extensions (read, flows, program operations, and the program read descriptor) under symbol keys. Spreading them ({ ...MY_STACK }) keeps those extensions, while Object.keys and JSON.stringify do not list them.
createSession(definition, options?)
Section titled “createSession(definition, options?)”Creates the console that connects generated stacks and standalone programs. Programs packaged by stacks are automatically promoted to session.programs by reference.
Parameters:
definition.stacks— Named generated stack definitions (optional)definition.programs— Named standalone program definitions (optional)definition.protocols— Named generated protocol definitions, available atsession.protocols.<name>; their stacks and programs joinsession.stacksandsession.programs(optional)options.auth— Authentication for every member, such as{ publishableKey }in a browser or{ secretKey }on a server (optional)options.fetch— Custom fetch implementation (optional)options.wallet— Shared wallet for execution (optional)options.signerRegistry— Shared additional signers (optional)options.transport— Default member transport,'ws'(the default) or'http'(optional)options.transactions— Transaction transport override (optional)options.endpoints— Shared HTTP and WebSocket fallback endpoints (optional)options.stacks— Per-stack connection overrides, includingurl,httpUrl, andtransport(optional)options.stacks.<name>.autoConnect— Per-stack initial connection control; defaults totrue(optional)options.stacks.<name>.autoReconnect— Per-stack post-connection recovery control; defaults totrue(optional)options.protocols.<name>— Per-protocol client options (optional)
At least one stack, program, or protocol is required.
Returns: Promise<Session>
In React, AreteSessionProvider calls createSession for you. See
Compose a session in React.
session.stacks.<name>
Section titled “session.stacks.<name>”Returns the connected generated stack inserted under that name. Use its views, queries, programs, addresses, constants, defaults, math, read, and flows namespaces when present.
Every connected client, including a session member, also exposes stack, the stack definition it was connected for (with any attached programs), and transport: 'ws' when it opened the streaming WebSocket, 'http' when it did not. A member reports the transport its session connected it with. React’s useArete(session.stacks.<name>) reads both.
session.programs.<name>
Section titled “session.programs.<name>”Returns a connected program packaged by a stack, attached to a stack, or declared standalone. The same program SDK reached several ways is one object; see Multiple Stacks and Programs for how a key claimed by two programs resolves. Program namespaces are programId, schemas, pdas, addresses, accounts, queries, raw, instructions, transactions, flows, constants, defaults, and math.
session.protocols.<name>
Section titled “session.protocols.<name>”Returns the protocol client registered under that name. It shares the session’s auth; see Integrate a protocol.
session.chain
Section titled “session.chain”The session-wide generic Solana read surface.
session.wallet and session.setWallet(wallet)
Section titled “session.wallet and session.setWallet(wallet)”session.wallet is the wallet every member signs with, or undefined. session.setWallet(wallet) replaces it without reconnecting anything; pass undefined when the user disconnects.
session.execute(prepared, options?)
Section titled “session.execute(prepared, options?)”Executes a value returned by a generated instruction, transaction, or flow’s
.prepare(...) method.
Arete.connect(stack, options?)
Section titled “Arete.connect(stack, options?)”Creates a direct single-stack client. This existing API remains useful for advanced per-connection options such as url, maxEntriesPerView, and validateFrames.
session.stacks.<name>.views
Section titled “session.stacks.<name>.views”Typed views interface based on your stack definition. Access pattern:
session.stacks.<name>.views.<entity>.<viewName>.<method>()
// Examples with default views:session.stacks.ore.views.OreRound.state.get(key) // State mode — requires keysession.stacks.ore.views.OreRound.list.get() // List mode — no keyState Mode Methods
Section titled “State Mode Methods”For views in state mode (keyed lookup). All methods require a key parameter:
| Method | Signature | Returns |
|---|---|---|
use | use(key, options?) | AsyncIterable<T> |
watch | watch(key, options?) | AsyncIterable<Update<T>> |
watchRich | watchRich(key, options?) | AsyncIterable<RichUpdate<T>> |
get | get(key, options?) | Promise<T | null> |
getSync | getSync(key, options?) | T | null | undefined |
List Mode Methods
Section titled “List Mode Methods”For views in list mode (collections). No key parameter:
| Method | Signature | Returns |
|---|---|---|
use | use(options?) | AsyncIterable<T> |
watch | watch(options?) | AsyncIterable<Update<T>> |
watchRich | watchRich(options?) | AsyncIterable<RichUpdate<T>> |
get | get(options?) | Promise<T[]> |
getOne | getOne(options?) | Promise<T | null> |
getSync | getSync(options?) | T[] | undefined |
WatchOptions
Section titled “WatchOptions”Options for streaming methods:
interface WatchOptions { partition?: string; filters?: Record<string, unknown>; take?: number; skip?: number; withSnapshot?: boolean; after?: string; snapshotLimit?: number; schema?: Schema<T>;}See Schema Validation for details on using schemas to filter and validate streamed data.
GetOptions
Section titled “GetOptions”get and getOne accept the query options above plus a bound on the wait:
interface GetOptions extends WatchOptions { /** Reject with InitialDataTimeoutError after this long (default 5000). null waits forever. */ timeoutMs?: number | null;}session.stacks.<name>.connectionState
Section titled “session.stacks.<name>.connectionState”Current connection state:
disconnected— Not connectedconnecting— Establishing connectionconnected— Active connectionreconnecting— Auto-reconnecting after failureerror— Connection failed
session.stacks.<name>.onConnectionStateChange(callback)
Section titled “session.stacks.<name>.onConnectionStateChange(callback)”Subscribe to connection state changes. Returns unsubscribe function.
session.close()
Section titled “session.close()”Closes every connection owned by the session.
session.platform and client.platform
Section titled “session.platform and client.platform”The authenticated Arete API client, a PlatformHttpClient, when the auth
carries an API credential (a secret key, or tokenKind: 'api'), and
undefined otherwise. A session shares its client with every member that has
no auth of its own, and SDK extensions receive it as context.platform. Set
apiUrl or pass your own platform in the session or connect options. See
Arete API client.
createPlatformHttpClient(options?)
Section titled “createPlatformHttpClient(options?)”Creates a PlatformHttpClient for Arete API calls with the caller’s own
credential. options.apiUrl defaults to https://api.arete.run;
options.auth takes secretKey, token, getToken or tokenEndpoint, and a
publishable key throws INVALID_CONFIG; options.fetch overrides fetch.
request({ path, method?, body?, signal? }) resolves to { status, body }. See
Arete API client.
Next Steps
Section titled “Next Steps”- Schema Validation — Zod schemas for runtime validation and filtering
- React SDK — Hooks and providers for React apps
- Rust SDK — Native Rust client
- Resolvers — Enrich entities with token metadata and computed fields
- Build Your Own Stack — Create custom data streams