Skip to content

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 .md to the URL or sending Accept: 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.


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

Terminal window
npm install @usearete/sdk

No peer dependencies. Works anywhere JavaScript runs.


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 safety
for 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.


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:

NamespacePurpose
viewsTyped live and point-in-time entity views
queriesStack-level HTTP queries
programsPrograms packaged with the stack
addressesDomain address derivations, when defined
constantsDomain constants and enums, when defined
defaultsReusable domain defaults, when defined
mathPure domain calculations, when defined
readConnected domain reads, when defined
flowsStack-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.

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]);

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 views
const positions = await session.stacks.app.views.Position.list.get({ take: 20 });
// Generic chain reads belong to the session console
const lamports = await session.chain.lamports(owner);

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, so session.programs.ore === session.stacks.ore.programs.ore and there is one client, not two. Configure it through the stack member’s options. The exception is a standalone program whose programReads.<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 an AreteError with code PROGRAM_KEY_CONFLICT before 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 takes session.programs.<key> while session.stacks.<name>.programs.<key> keeps the stack’s program. When two stacks bundle such copies, the first stack’s program takes session.programs.<key>.
  • Anything else is a conflict. A different or missing programSpecHash throws PROGRAM_KEY_CONFLICT. The message names the stack and key: use session.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 through session.stacks, and reading session.programs.<key> throws PROGRAM_KEY_CONFLICT with 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.

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",
});
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
});
OptionTypeDefaultDescription
urlstringstack.urlOverride the stack’s default URL
maxEntriesPerViewnumber | null10000Max entries per view before LRU eviction
autoConnectbooleantrueStart the initial WebSocket connection
autoReconnectbooleantrueRecover after an established WebSocket connection is lost
validateFramesbooleantrueLog generated-schema frame rejections
onFrameValidationErrorfunction—Observe structured generated-schema frame rejections
apiUrlstringhttps://api.arete.runArete API origin for client.platform
platformPlatformHttpClientbuilt from authArete 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.

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) or environment: { kind: 'dev' }: the dev environment. The target is the edge a4 dev up serves (ARETE_DEV_URL, or found from the running a4 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.

// Check current state
console.log(session.stacks.app.connectionState);
// 'connected' | 'connecting' | 'disconnected' | 'reconnecting' | 'error'
// Listen for changes
const unsubscribe = session.stacks.app.onConnectionStateChange((state) => {
console.log("Connection state:", state);
});
// Later: stop listening
unsubscribe();
session.close();

Choose the credential by where the code runs and who it acts for:

authRuns inActs for
publishableKey (a4_pk_...)A browserA visitor to your app, bound to its origins
secretKey (a4_ak_... or a4_sk_...)A server or scriptYour Arete account
token, getToken or tokenEndpointAnywhereThe target your backend minted the session token for
token, getToken or tokenEndpoint with tokenKind: 'api'A browser or serverAn 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.

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 apiUrl you set in code (createSession, Arete.connect, createPlatformHttpClient or a protocol client), for a local, development or self-hosted API. This only covers credentials you pass in auth.

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.

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, getToken or tokenEndpoint, the credential falls back to ARETE_API_KEY, then to the a4 login.
  • 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 static token cannot 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.

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.

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.

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 to https://api.arete.run. Setting it trusts that origin for the credential in auth.
  • platform: an Arete API client to use instead of the one built from auth.

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, getToken or tokenEndpoint, each read as an API credential. Outside browsers an unset auth falls back to ARETE_API_KEY, then to the a4 login.
  • 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.


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.


Every view operates in one of two modes, which determines how you access data:

ModeDescriptionKey RequiredReturns
StateLookup individual entities by keyYesSingle entity (T | null)
ListAccess a collection of entitiesNoArray of entities (T[])

By default, each entity in a stack exposes two views:

View NameModeDescription
stateStateLookup any entity by its key (e.g., address)
listListCollection of all entities
// State view — get a specific round with its generated key
const round = await session.stacks.ore.views.OreRound.state.get({ roundId });
// List view — get all rounds
const rounds = await session.stacks.ore.views.OreRound.list.get();

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 stack
for 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 return AsyncIterable and continuously emit data as entities change. The connection stays open until you break out of the loop or abort.

MethodEmitsUse Case
.use()TSimplest — 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

The simplest streaming method. Emits the full merged entity after each change:

// List view — no key required
for 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 required
for 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 key
use(key: TKey, options?: WatchOptions): AsyncIterable<T>
// List view
use(options?: WatchOptions): AsyncIterable<T>

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 view
watch(key: TKey, options?: WatchOptions): AsyncIterable<Update<T>>
// List view
watch(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 view
watchRich(key: TKey, options?: WatchOptions): AsyncIterable<RichUpdate<T>>
// List view
watchRich(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 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.

MethodReturnsBehavior
.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 | undefinedSync — reads an already active subscription, undefined if none

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 entities
const rounds = await session.stacks.ore.views.OreRound.latest.get();
console.log(`Found ${rounds.length} rounds`);
// State view — returns single entity or null
const 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 found
get(key: TKey, options?: GetOptions): Promise<T | null>
// List view — returns array of all entities
get(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`);
}
}

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>

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 view
const rounds = session.stacks.ore.views.OreRound.latest.getSync();
if (rounds) {
console.log(`Cached: ${rounds.length} rounds`);
} else {
console.log("Data not yet loaded");
}
// State view
const 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[] | undefined

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
});

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.

// Only receive the first 10 entities
for await (const round of session.stacks.ore.views.OreRound.latest.use({ take: 10 })) {
console.log("Round:", round.id.roundId);
}
// Skip first 20, take next 10
for await (const round of session.stacks.ore.views.OreRound.latest.use({ skip: 20, take: 10 })) {
console.log("Round:", round.id.roundId);
}

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.


Use standard AsyncIterable patterns to control streams client-side.

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;
}
}
}

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;
}
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);
}

Every stack ships with Zod schemas alongside its TypeScript interfaces. Use them to validate data at two levels:

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.

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.

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.


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.


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 units

safeToRawAmount returns { success: true, data } | { success: false, error } instead of throwing for invalid amount input. It is also re-exported by @usearete/react.


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;
}
}

@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.

HelperPurpose
createWebSocketHarness()Scripted WebSocket server for a real client: pass harness.websocketFactory as auth.websocketFactory, then await subscriptions and answer them with frames
framesBuilders 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 upsert
ws.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.


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.

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 at session.protocols.<name>; their stacks and programs join session.stacks and session.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, including url, httpUrl, and transport (optional)
  • options.stacks.<name>.autoConnect — Per-stack initial connection control; defaults to true (optional)
  • options.stacks.<name>.autoReconnect — Per-stack post-connection recovery control; defaults to true (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.

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.

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.

Returns the protocol client registered under that name. It shares the session’s auth; see Integrate a protocol.

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.

Executes a value returned by a generated instruction, transaction, or flow’s .prepare(...) method.

Creates a direct single-stack client. This existing API remains useful for advanced per-connection options such as url, maxEntriesPerView, and validateFrames.

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 key
session.stacks.ore.views.OreRound.list.get() // List mode — no key

For views in state mode (keyed lookup). All methods require a key parameter:

MethodSignatureReturns
useuse(key, options?)AsyncIterable<T>
watchwatch(key, options?)AsyncIterable<Update<T>>
watchRichwatchRich(key, options?)AsyncIterable<RichUpdate<T>>
getget(key, options?)Promise<T | null>
getSyncgetSync(key, options?)T | null | undefined

For views in list mode (collections). No key parameter:

MethodSignatureReturns
useuse(options?)AsyncIterable<T>
watchwatch(options?)AsyncIterable<Update<T>>
watchRichwatchRich(options?)AsyncIterable<RichUpdate<T>>
getget(options?)Promise<T[]>
getOnegetOne(options?)Promise<T | null>
getSyncgetSync(options?)T[] | undefined

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.

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;
}

Current connection state:

  • disconnected — Not connected
  • connecting — Establishing connection
  • connected — Active connection
  • reconnecting — Auto-reconnecting after failure
  • error — Connection failed

session.stacks.<name>.onConnectionStateChange(callback)

Section titled “session.stacks.<name>.onConnectionStateChange(callback)”

Subscribe to connection state changes. Returns unsubscribe function.

Closes every connection owned by the session.

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.

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.